Skip to content

Merge main into release/6.2 #1361

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

Merged
merged 14 commits into from
Jun 17, 2025
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
3 changes: 3 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ jobs:
with:
linux_swift_versions: '["nightly-main"]'
windows_swift_versions: '["nightly-main"]'
enable_macos_checks: false
macos_xcode_versions: '["16.3"]'
macos_versions: '["sequoia"]'

soundness:
name: Soundness
Expand Down
23 changes: 18 additions & 5 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ let wasiLibcCSettings: [CSetting] = [
.define("_WASI_EMULATED_MMAN", .when(platforms: [.wasi])),
]

var testOnlySwiftSettings: [SwiftSetting] = [
// The latest Windows toolchain does not yet have exit tests in swift-testing
.define("FOUNDATION_EXIT_TESTS", .when(platforms: [.macOS, .linux, .openbsd]))
]

#if os(Linux)
import FoundationEssentials

if ProcessInfo.processInfo.operatingSystemVersionString.hasPrefix("Ubuntu 20.") {
// Exit tests currently hang indefinitely on Ubuntu 20.
testOnlySwiftSettings.removeFirst()
}
#endif

let package = Package(
name: "swift-foundation",
platforms: [.macOS("15"), .iOS("18"), .tvOS("18"), .watchOS("11")],
Expand Down Expand Up @@ -171,7 +185,7 @@ let package = Package(
"LifetimeDependenceMutableAccessors",
.when(platforms: [.macOS, .iOS, .watchOS, .tvOS, .linux])
),
] + availabilityMacros + featureSettings
] + availabilityMacros + featureSettings + testOnlySwiftSettings
),

// FoundationInternationalization
Expand Down Expand Up @@ -204,7 +218,7 @@ let package = Package(
"TestSupport",
"FoundationInternationalization",
],
swiftSettings: availabilityMacros + featureSettings
swiftSettings: availabilityMacros + featureSettings + testOnlySwiftSettings
),

// FoundationMacros
Expand Down Expand Up @@ -233,10 +247,9 @@ package.targets.append(contentsOf: [
.testTarget(
name: "FoundationMacrosTests",
dependencies: [
"FoundationMacros",
"TestSupport"
"FoundationMacros"
],
swiftSettings: availabilityMacros + featureSettings
swiftSettings: availabilityMacros + featureSettings + testOnlySwiftSettings
)
])
#endif
113 changes: 113 additions & 0 deletions Proposals/0027-UTCClock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# UTCClock and Epochs

* Proposal: [SF-0027](0027-UTCClock.md)
* Authors: [Philippe Hausler](https://github.com/phausler)
* Review Manager: [Tina L](https://github.com/itingliu)
* Status: Review: Jun 10...Jun 17, 2025
* Implementation: [Pull request](https://github.com/swiftlang/swift-foundation/pull/1344)
* Review: [Pitch](https://forums.swift.org/t/pitch-utcclock/78018)

## Introduction

[The proposal for Clock, Instant and Duration](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0329-clock-instant-duration.md) was left with a future direction to address feedback for the need of clocks based upon the time measured by interacting with the user displayed clock, otherwise known as the "wall clock".

This proposal introduces a new clock type for interacting with the user displayed clock, transacts in instants that are representations of offsets from an epoch and defined by the advancement from a UTC time.

## Motivation

Clocks in general can express many different mechanisms for time. That time can be strictly increasing, increasing but paused when the computer is not active, or in a clock in the traditional non-computing sense that one schedules according to a given time of day. The latter one must have some sort of mechanism to interact with calendarical and localized calculations.

All three of the aforementioned clocks all have a concept of a starting point of reference. This is not a distinct requirement for creating a clock, but all three share this additional convention of a property.

## Proposed solution and example

In short, a new clock will be added: `UTCClock`. This clock will have its `Instant` type defined as `Date`. There will also be affordances added for calculations that account for the edge cases of [leap seconds](https://en.wikipedia.org/wiki/Leap_second) (which currently `Date` on its own does not currently offer any sort of mechanism either on itself or via `Calendar`). `Date` has facilities for expressing a starting point from an epoch, however that mechanism is not shared to `ContinuousClock.Instant` or `SuspendingClock.Instant`. All three types will have an added new static property for fetching the `epoch` - and it is suggested that any adopters of `InstantProtocol` should add a new property to their types to match this convention where it fits.

Usage of this `UTCClock` can be illustrated by awaiting to perform a task at a given time of day. This has a number of interesting wrinkles that the `SuspendingClock` and `ContinousClock` wouldn't be able to handle. Example cases include where the deadline might be beyond a daylight savings time. Since `Date` directly interacts with `Calendar` it then ensures appropriate handling of these edges and is able to respect the user's settings and localization.

```swift
let calendar = Calendar.current
var when = calendar.dateComponents([.day, .month, .year], from: .now)
when.day = when.day.map { $0 + 1 }
when.hour = 8

if let tomorrowMorning8AM = calendar.date(from: when) {
try await UTCClock().sleep(until: tomorrowMorning8AM)
playAlarmSound()
}
```

This can be used not only for alarms, but also scheduled maintenance routines or other such chronological tasks. The applications for which span from mobile to desktop to server environments and have a wide but specific set of use cases. It is worth noting that this new type should not be viewed as a replacement since those others have key functionality for representing behavior where the concept of time would be inappropriate to be non-monotonic.


## Detailed design

These additions can be broken down into three categories; the `UTCClock` definition, the conformance of `Date` to `InstantProtocol`, and the extensions for vending epochs.

The structure of the `UTCClock` is trivially sendable since it houses no specific state and has the defined typealias of its `Instant` as `Date`. The minimum feasible resolution of `Date` is 1 nanosecond (however that may vary from platform to platform where Foundation is implemented).

```swift
@available(FoundationPreview 6.2, *)
public struct UTCClock: Sendable {
public typealias Instant = Date
public init()
}

@available(FoundationPreview 6.2, *)
extension UTCClock: Clock {
public func sleep(until deadline: Date, tolerance: Duration? = nil) async throws
public var now: Date { get }
public var minimumResolution: Duration { get }
}
```

The extension of `Date` conforms it to `InstantProtocol` and adds one addition "near miss" of the protocol as an additional function that in practice feels like a default parameter. This `duration(to:includingLeapSeconds:)` function provides the calculation of the duration from one point in time to another and calculates if the span between the two points includes a leap second or not. This calculation can be used for historical astronomical data since the irregularity of the rotation causes variation in the observed solar time. Those points are historically fixed and are a known series of events at specific dates (in UTC)[^utclist].

```swift
@available(FoundationPreview 6.2, *)
extension Date: InstantProtocol {
public func advanced(by duration: Duration) -> Date
public func duration(to other: Date) -> Duration
}

@available(FoundationPreview 6.2, *)
extension Date {
public static func leapSeconds(from start: Date, to end: Date) -> Duration
}
```

Usage of the `duration(to:)` and `leapSeconds(from:to:)` works as follows to calculate the total number of leap seconds:

```swift
let start = Calendar.current.date(from: DateComponents(timeZone: .gmt, year: 1971, month: 1, day: 1))!
let end = Calendar.current.date(from: DateComponents(timeZone: .gmt, year: 2017, month: 1, day: 1))!
let leaps = Date.leapSeconds(from: start, to: end)
print(leaps) // prints 27.0 seconds
print(start.duration(to: end) + leaps) // prints 1451692827.0 seconds
```

It is worth noting that the usages of leap seconds for a given range is not a common use in most every-day computing; this is intended for special cases where data-sets or historical leap second including durations are strictly needed. The general usages should only require the normal `duration(to:)` api without adding any additional values. Documentation of this function will reflect that more "niche" use case.

An extension to `UTCClock` will be made in `Foundation` for exposing an `systemEpoch` similarly to the properties proposed for the `SuspendingClock` and `ContinousClock`. This epoch will be defined as the `Date(timeIntervalSinceReferenceDate: 0)` which is Jan 1 2001.

```swift

@available(FoundationPreview 6.2, *)
extension UTCClock {
public static var systemEpoch: Date { get }
}
```

## Impact on existing code

This is a purely additive set of changes.

## Alternatives considered

It was considered to add a protocol joining the epochs as a "EpochInstant" but that offers no real algorithmic advantage worth introducing a new protocol. Specialization of functions should likely just use where clauses to the specific instant or clock types.

It was considered to add a new `Instant` type instead of depending on `Date` however this was rejected since it would mean re-implementing a vast swath of the supporting APIs for `Calendar` and such. The advantage of this is minimal and does not counteract the additional API complexity.

It was considered to add a near-miss overload for `duration(to:)` that had an additional parameter of `includingLeapSeconds` this was dismissed because it was considered to be too confusing and may lead to bugs where that data was not intended to be used.

[^utclist] If there are any updates to the list that will be considered a data table update and require a change to Foundation.
198 changes: 198 additions & 0 deletions Proposals/0028-locale-region-category.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# `Locale.Region.Category`

* Proposal: [SF-0028](0028-locale-region-category.md)
* Authors: [Tina Liu](https://github.com/itingliu)
* Review Manager: [Jeremy Schonfeld](https://github.com/jmschonfeld)
* Implementation: [https://github.com/swiftlang/swift-foundation/pull/1253]()
* Status: **Review: Jun 11, 2025...Jun 18, 2025**
* Reviews: [Pitch](https://forums.swift.org/t/pitch-category-support-for-locale-region/79240)

## Introduction

Currently, `Locale.Region` offers a few functionalities to query information about a region:

```swift
extension Locale.Region {
/// An array of regions defined by ISO.
public static var isoRegions: [Locale.Region]

/// The region that contains this region, if any.
public var containingRegion: Locale.Region? { get }

/// The continent that contains this region, if any.
public var continent: Locale.Region? { get }

/// An array of all the sub-regions of the region.
public var subRegions: [Locale.Region] { get }
}
```

Here are some examples of how you can use it:

```swift
let argentina = Locale.Region.argentina

_ = argentina.continent // "019" (Americas)
_ = argentina.containingRegion // "005" (South America)
```

We'd like to propose extending `Locale.Region` to support grouping the return result by types, such as whether it's a territory or continent.

One use case for this is for UI applications to display supported regions or offer UI to select a region, similar to the Language & Region system settings on mac and iOS. Instead of showing all supported regions returned by `Locale.Region.isoRegions` as a flat list, clients will be able to have a hierarchical list that groups the regions by types such as continents.

## Proposed solution and example

We propose adding `struct Locale.Region.Category` to represent different categories of `Locale.Region`, and companion methods to return results matching the specified category. There are also a few existing API that provides information about the region. We propose adding `subcontinent` to complement the existing `.continent` property.

You can use it to get regions of specific categories:

```swift
let argentina = Locale.Region.argentina
_ = argentina.category // .territory
_ = argentina.subcontinent // "005" (South America)

let americas = Locale.Region("019")
_ = americas.category // .continent
_ = americas.subRegions(ofCategory: .subcontinent) // All subcontinents in Americas: ["005", "013", "021", "029"] (South America, Central America, Northern America, Caribbean)
_ = americas.subRegions(ofCategory: .territory) // All territories in Americas

_ = Locale.Region.isoRegions(ofCategory: .continent) // All continents: ["002", "009", "019", "142", "150"] (Africa, Oceania, Americas, Asia, Europe)
```


## Detailed design

```swift
@available(FoundationPreview 6.2, *)
extension Locale.Region {

/// Categories of a region. See https://www.unicode.org/reports/tr35/tr35-35/tr35-info.html#Territory_Data
public struct Category: Codable, Sendable, Hashable, CustomDebugStringConvertible {
/// Category representing the whole world.
public static let world: Category

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

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

/// Category representing a territory.
public static let territory: Category

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

/// An array of regions matching the specified categories.
public static func isoRegions(ofCategory category: Category) -> [Locale.Region]

/// The category of the region, if any.
public var category: Category? { get }

/// An array of the sub-regions, matching the specified category of the region.
/// If `category` is higher in the hierarchy than `self`, returns an empty array.
public func subRegions(ofCategory category: Category) -> [Locale.Region]

/// The subcontinent that contains this region, if any.
public var subcontinent: Locale.Region?
}
```

### `Locale.Region.Category`

This type represents the territory containment levels as defined in [Unicode LDML #35](https://www.unicode.org/reports/tr35/tr35-35/tr35-info.html#Territory_Data). An overview of the latest categorization is available [here](https://www.unicode.org/cldr/charts/46/supplemental/territory_containment_un_m_49.html).

Currently, `.world` is only associated with `Locale.Region("001")`. `.territory` includes, but is not limited to, countries, as it also includes regions such as Antarctica (code "AQ"). `.grouping` is a region that has well defined membership, such as European Union (code "EU") and Eurozone (code "EZ"). It isn't part of the hierarchy formed by other categories.

### Getting sub-regions matching the specified category

```swift
extension Locale.Region {
public func subRegions(ofCategory category: Category) -> [Locale.Region]
}
```

If the value is higher up in the hierarchy than that of `self`, the function returns an empty array.

```swift
argentina.subRegions(in: .world) // []
```

On the other hand, the specified `category` that is more than one level down than that of `self` is still valid, as seen previously in the ["Proposed solution and example" section](#proposed-solution-and-example)

```swift
// Passing both `.subcontinent` and `.territory` as the argument are valid
_ = americas.subRegions(ofCategory: .subcontinent) // All subcontinents in Americas
_ = americas.subRegions(ofCategory: .territory) // All territories in Americas
```

## Impact on existing code

### `Locale.Region.isoRegions` starts to include regions of the "grouping" category

Currently `Locale.Region.isoRegions` does not return regions that fall into the `.grouping` category. Those fall under the grouping category don't fit into the tree-structured containment hierarchy like the others. Given that it is not yet possible to filter ISO regions by category, these regions are not included in the return values of API.

With the introduction of `Locale.Region.isoRegions(ofCategory:)`, we propose changing the behavior of `Locale.Region.isoRegions` to include all ISO regions, including those of the grouping category. Those who wish to exclude those of the "grouping" category can do so with `Locale.Region.isoRegions(of:)`.

Please refer to the Alternative Considered section for more discussion.

## Alternatives considered

### Naming consideration: `Locale.Region.Category`

ICU uses `URegionType` to represent the categories, while Unicode uses the term "territory containment (level)". We considered introducing `Category` as `Type`, `Containment`, `ContainmentLevel`, or `GroupingLevel`.

`Type` was not the optimal choice because not only it is a language keyword, but also overloaded. `Containment`, `ContainmentLevel` or `GroupingLevel` would all be good fits for modeling regions as a tree hierarchy, but we never intend to force the hierarchy idea onto `Locale.Region`, and the "grouping" category is not strictly a containment level either.

`Category` shares similar meanings to `Type`, with less strict containment notion, and is typically used in API names.

### Introduce `containingRegion(ofCategory:)`

An alternative is to introduce a method such as `containingRegion(ofCategory:)` to return the containing region of the specified category:

```swift
extension Locale.Region {
/// The containing region, matching the specified category of the region.
public func containingRegion(ofCategory category: Category) -> Locale.Region?
}
```

Developers would use it like this:

```swift
// The continent containing Argentina, equivalent to `argentina.continent`
_ = argentina.containingRegion(ofCategory: .continent) // "019" (Americas)

// The sub-continent containing Argentina, equivalent to `argentina.subcontinent`
_ = argentina.containingRegion(ofCategory: .subcontinent) // "005" (South America)
```

Functionally it would be equivalent to existing `public var continent: Locale.Region?`. Having two entry points for the same purpose would be more confusing than helpful, so it was left out for simplicity.

### Naming consideration: `ofCategory` argument label

Since the "category" in the argument label in the proposed functions is the name of the type, it would be acceptable to omit it from the label, so

```swift
public func subRegions(ofCategory category: Category) -> [Locale.Region]
```

would become

```swift
public func subRegions(of category: Category) -> [Locale.Region]
```

However, this reads less fluent from the call-site:

```swift
let continent = Locale.Region(<some continentCode>)
let territories = continent.subRegions(of: .territory)
```

It seems to indicate `territories` is "the continent's subregions of (some) territory", as opposed to the intended "the content's subregions **of category** 'territory'". Therefore it is left in place to promote fluent usage.

### Do not change the behavior of `Locale.Region.isoRegions`

It was considered to continue to omit regions that fall into the "grouping" category from `Locale.Region.isoRegions` for compatibility. However, just like all the other Locale related API, the list of ISO regions is never guaranteed to be constant. We do not expect users to rely on its exact values, so compatibility isn't a concern.
Loading
Loading