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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Change Log

## [1.1.0] - 2026-06-26

### Changed

- Adopted Swift's Approachable Concurrency upcoming features
(`NonisolatedNonsendingByDefault` and `InferIsolatedConformances`). The public
`async` entry points (`CIFP(url:)`, `CIFP(bytes:)`, and `linked()`) now run on
the caller's executor by default, and the streaming line readers are annotated
`@concurrent` so file and byte iteration keep running off the caller's
executor. No public signatures changed.

### Internal

- Removed the remaining `nonisolated(unsafe)` escape hatches: the two
header-parsing regexes are now compiled once per parse on the builder instead
of stored as shared unsafe statics.
- Dropped a vestigial `@preconcurrency` from `import RegexBuilder`; the module is
fully `Sendable`-audited under Swift 6.

## [1.0.0] - 2026-01-17

Initial release.
14 changes: 11 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

import PackageDescription

let approachableConcurrency: [SwiftSetting] = [
.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
.enableUpcomingFeature("InferIsolatedConformances")
]

let package = Package(
name: "SwiftCIFP",
defaultLocalization: "en",
Expand All @@ -22,11 +27,13 @@ let package = Package(
targets: [
.target(
name: "SwiftCIFP",
resources: [.process("Resources")]
resources: [.process("Resources")],
swiftSettings: approachableConcurrency
),
.testTarget(
name: "SwiftCIFPTests",
dependencies: ["SwiftCIFP"]
dependencies: ["SwiftCIFP"],
swiftSettings: approachableConcurrency
)
],
swiftLanguageModes: [.v5, .v6]
Expand All @@ -41,7 +48,8 @@ let package = Package(
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "ZIPFoundation", package: "ZIPFoundation"),
.product(name: "Progress", package: "Progress.swift")
]
],
swiftSettings: approachableConcurrency
)
)
#endif
30 changes: 16 additions & 14 deletions Sources/SwiftCIFP/CIFP.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
#if canImport(CoreLocation)
import CoreLocation
#endif
@preconcurrency import RegexBuilder
import RegexBuilder

/// Container for CIFP (Coded Instrument Flight Procedures) data.
///
Expand Down Expand Up @@ -361,32 +361,34 @@ func expandRunwayTransitionId(_ transitionId: String) -> [String] {

/// Builder for aggregating parsed records into CIFP.
private struct CIFPBuilder {
// MARK: - Static Regex Patterns
// MARK: - Date Parsing

/// Parses the creation date from HDR01.
private static let creationDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MMM-yyyy"
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()

// MARK: - Header-Parsing Patterns

/// Matches "VOLUME" followed by whitespace and a 4-digit cycle number.
nonisolated(unsafe) private static let volumeCycleRegex = Regex {
private let volumeCycleRegex = Regex {
"VOLUME"
OneOrMore(.whitespace)
Capture { Repeat(.digit, count: 4) }
}

/// Matches a date in DD-MMM-YYYY format (e.g., "15-JAN-2024").
nonisolated(unsafe) private static let creationDateRegex = Regex {
private let creationDateRegex = Regex {
Repeat(.digit, count: 2)
"-"
Repeat("A"..."Z", count: 3)
"-"
Repeat(.digit, count: 4)
}

/// Parses the creation date from HDR01.
private static let creationDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "dd-MMM-yyyy"
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}()

var headerRecords: [HeaderRecord] = []
var gridMORAs: [GridMORA] = []
var vhfNavaids: [String: VHFNavaid] = [:]
Expand Down Expand Up @@ -569,7 +571,7 @@ private struct CIFPBuilder {
}
// Fall back to looking for "VOLUME XXXX" pattern
for text in headerText {
guard let match = text.firstMatch(of: Self.volumeCycleRegex) else { continue }
guard let match = text.firstMatch(of: volumeCycleRegex) else { continue }
let cycleStr = String(match.1)
guard let c = Cycle(yymm: cycleStr) else { continue }
return c
Expand Down Expand Up @@ -1080,7 +1082,7 @@ private struct CIFPBuilder {

private func parseCreationDate(from hdr01: String) -> DateComponents? {
// Look for DD-MMM-YYYY pattern
guard let match = hdr01.firstMatch(of: Self.creationDateRegex) else {
guard let match = hdr01.firstMatch(of: creationDateRegex) else {
return nil
}
let dateStr = String(match.0)
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftCIFP/Parser/CIFPLineReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ where Source.Element == UInt8, Source: Sendable {
lineBuffer.reserveCapacity(lineBufferCapacity)
}

@concurrent
mutating func next() async throws -> [UInt8]? {
lineBuffer.removeAll(keepingCapacity: true)

Expand Down
Loading