Skip to content

Commit ff6bb24

Browse files
committed
Swift 6 concurrency mode
API changes: * Compatible with Swift 6 concurrency mode * METAR, TAF, and associated objects all now Sendable * parse functions are now async * Added Visibility.notRecorded case to differentiate "VSNO" from "M" * More lenient parsing of direction ranges * Minimum OS versions advanced * Removed unused Error case Internal changes: * Uses new RegexBuilder API instead of NSRegularExpression * METAR, TAF, and Remarks parsers are now actors that preload their Regex instances * Individual component parsers are now classes instead of functions * Added RegexCases protocol for enums that are parsed from a Regex * Stricter handling of TAF periods that cross month boundaries * Moved remarks files at top level alongside METAR and TAF files Test changes: * Uses async support in Quick * Added more test cases for fractional distances and direction ranges Supporting changes: * Gauntlet now uses ArgumentParser * Gauntlet no longer uses gzipped URLs, to support async streaming
1 parent e6dd107 commit ff6bb24

File tree

228 files changed

+3817
-2791
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

228 files changed

+3817
-2791
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@ jobs:
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
swift: ["5.9", "5.10"]
20-
os: [macos-14]
19+
swift: ["6.0"]
20+
os: [macos-15]
2121
runs-on: ${{ matrix.os }}
2222
steps:
23-
- uses: actions/checkout@v3
23+
- uses: actions/checkout@v4
2424
- uses: swift-actions/setup-swift@v2
2525
with:
2626
swift-version: ${{ matrix.swift }}

.github/workflows/doc.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ jobs:
2222
name: Generate Documentation
2323
runs-on: macos-latest
2424
steps:
25-
- uses: actions/checkout@v3
25+
- uses: actions/checkout@v4
2626
- uses: swift-actions/setup-swift@v2
2727
with:
28-
swift-version: "5.10"
28+
swift-version: "6.0"
2929
- name: Build
3030
run: |
3131
swift package \
@@ -35,7 +35,7 @@ jobs:
3535
--transform-for-static-hosting \
3636
--hosting-base-path SwiftMETAR
3737
- name: Upload
38-
uses: actions/upload-pages-artifact@v1
38+
uses: actions/upload-pages-artifact@v3
3939
with:
4040
path: "docs/"
4141

@@ -49,4 +49,4 @@ jobs:
4949
steps:
5050
- name: Deploy to GitHub Pages
5151
id: deployment
52-
uses: actions/deploy-pages@v2
52+
uses: actions/deploy-pages@v4

Package.resolved

Lines changed: 37 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
// swift-tools-version:5.9
2-
// The swift-tools-version declares the minimum version of Swift required to build this package.
1+
// swift-tools-version: 6.0
32

43
import PackageDescription
54

65
let package = Package(
76
name: "SwiftMETAR",
87
defaultLocalization: "en",
9-
platforms: [.macOS(.v13), .iOS(.v16), .watchOS(.v9), .tvOS(.v16)],
8+
platforms: [.macOS(.v15), .iOS(.v18), .watchOS(.v11), .tvOS(.v18)],
109
products: [
1110
// Products define the executables and libraries a package produces, and make them visible to other packages.
1211
.library(
@@ -16,24 +15,27 @@ let package = Package(
1615
],
1716
dependencies: [
1817
// Dependencies declare other packages that this package depends on.
19-
.package(url: "https://github.com/Quick/Quick.git", from: "4.0.0"),
20-
.package(url: "https://github.com/Quick/Nimble.git", from: "9.2.0"),
21-
.package(url: "https://github.com/objecthub/swift-numberkit.git", from: "2.4.2"),
22-
.package(url: "https://github.com/sharplet/Regex.git", from: "2.1.0"),
23-
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.0.0"),
24-
.package(url: "https://github.com/1024jp/GzipSwift.git", from: "6.0.0"),
18+
.package(url: "https://github.com/Quick/Quick.git", from: "7.6.2"),
19+
.package(url: "https://github.com/Quick/Nimble.git", from: "13.6.0"),
20+
.package(url: "https://github.com/objecthub/swift-numberkit.git", from: "2.6.0"),
21+
.package(url: "https://github.com/apple/swift-docc-plugin.git", from: "1.4.3"),
22+
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.5.0")
2523
],
2624
targets: [
2725
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
2826
// Targets can depend on other targets in this package, and on products in packages this package depends on.
2927
.target(
3028
name: "SwiftMETAR",
31-
dependencies: [.product(name: "NumberKit", package: "swift-numberkit"), "Regex"]),
29+
dependencies: [.product(name: "NumberKit", package: "swift-numberkit")]),
3230
.testTarget(
3331
name: "SwiftMETARTests",
3432
dependencies: ["SwiftMETAR", "Quick", "Nimble"]),
3533
.executableTarget(
3634
name: "SwiftMETARGauntlet",
37-
dependencies: ["SwiftMETAR", .product(name: "Gzip", package: "GzipSwift")])
38-
]
35+
dependencies: [
36+
"SwiftMETAR",
37+
.product(name: "ArgumentParser", package: "swift-argument-parser")
38+
])
39+
],
40+
swiftLanguageModes: [.v5, .v6]
3941
)

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ To parse a METAR in String format, simply call `METAR.from`. You will get back a
4747
that you can query for weather information:
4848

4949
```swift
50-
let observation = try METAR.from(string: myString)
50+
let observation = try await METAR.from(string: myString)
5151
if let winds = observation.winds {
5252
switch winds {
5353
case let .direction(heading, speed, gust):

Sources/SwiftMETAR/Common/Altimeter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Foundation
22

33
/// A sea-level pressure altimeter setting.
4-
public enum Altimeter: Codable, Comparable {
4+
public enum Altimeter: Codable, Comparable, Sendable {
55

66
/**
77
An altimeter setting in inches of mercury (typical in the US).

Sources/SwiftMETAR/Common/Condition.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import Foundation
22

33
/// A sky condition, either a cloud layer or the presence of a clear sky.
4-
public enum Condition: Codable, Equatable {
5-
4+
public enum Condition: Codable, Equatable, Sendable {
5+
66
/// Sky clear below 12,000 feet (USA) or 25,000 feet (Canada). Typically
77
/// reported by automated ceilometers.
88
case clear
@@ -135,7 +135,7 @@ public enum Condition: Codable, Equatable {
135135
}
136136

137137
/// Types of vertical development that a cloud layer can have.
138-
public enum CeilingType: String, Codable, CaseIterable {
138+
public enum CeilingType: String, Codable, RegexCases, Sendable {
139139

140140
/// Layer consists of cumulonimbus clouds.
141141
case cumulonimbus = "CB"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
func parseLocationID(_ parts: inout Array<String.SubSequence>) throws -> String {
2+
guard !parts.isEmpty else { throw Error.badFormat }
3+
return String(parts.removeFirst())
4+
}
Lines changed: 63 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,71 @@
11
import Foundation
2-
import Regex
2+
@preconcurrency import RegexBuilder
33

4-
fileprivate let types = Condition.CeilingType.allCases
5-
.map { NSRegularExpression.escapedPattern(for: $0.rawValue) }
6-
.joined(separator: "|")
7-
fileprivate let conditionsRxStr = "^(FEW|SCT|BKN|OVC|VV)(\\d+)(\(types))?$"
8-
fileprivate let conditionsRx = try! Regex(string: conditionsRxStr)
4+
class ConditionsParser {
5+
private enum Coverage: String, RegexCases {
6+
case few = "FEW"
7+
case scattered = "SCT"
8+
case broken = "BKN"
9+
case overcast = "OVC"
10+
case verticalVis = "VV"
11+
}
912

10-
func parseConditions(_ parts: inout Array<String.SubSequence>) throws -> Array<Condition> {
11-
if parts.isEmpty { return [] }
12-
13-
var conditions = Array<Condition>()
14-
15-
while true {
16-
if parts.isEmpty { return conditions }
17-
let condStr = String(parts[0])
18-
19-
switch condStr {
20-
case "SKC":
21-
parts.removeFirst()
22-
return [.skyClear]
23-
case "CLR", "NCD":
24-
parts.removeFirst()
25-
return [.clear]
26-
case "NSC":
27-
parts.removeFirst()
28-
return [.noSignificantClouds]
29-
case "CAVOK":
30-
parts.removeFirst()
31-
return [.cavok]
32-
default:
33-
break
34-
}
35-
36-
if let match = conditionsRx.firstMatch(in: condStr) {
37-
parts.removeFirst()
38-
39-
guard let coverage = match.captures[0],
40-
let flightLevelStr = match.captures[1],
41-
let flightLevel = UInt(flightLevelStr) else { throw Error.invalidConditions(condStr) }
42-
let height = flightLevel*100
43-
44-
var type: Condition.CeilingType? = nil
45-
if let typeStr = match.captures[2] {
46-
guard coverage != "VV" else { throw Error.invalidConditions(condStr) }
47-
type = Condition.CeilingType(rawValue: typeStr)
48-
guard type != nil else {
49-
throw Error.invalidConditions(condStr)
50-
}
13+
private let coverageRef = Reference<Coverage>()
14+
private let heightRef = Reference<UInt>()
15+
private let ceilingTypeRef = Reference<Condition.CeilingType?>()
16+
private lazy var rx = Regex {
17+
Anchor.startOfSubject
18+
Capture(as: coverageRef) { try! Coverage.rx } transform: { .init(rawValue: String($0))! }
19+
Capture(as: heightRef) { Repeat(.digit, count: 3) } transform: { UInt($0)! * 100 }
20+
Capture(as: ceilingTypeRef) {
21+
Optionally(try! Condition.CeilingType.rx)
22+
} transform: { .init(rawValue: String($0)) }
23+
Anchor.endOfSubject
24+
}
25+
26+
func parse(_ parts: inout Array<String.SubSequence>) throws -> Array<Condition> {
27+
if parts.isEmpty { return [] }
28+
29+
var conditions = Array<Condition>()
30+
31+
while true {
32+
if parts.isEmpty { return conditions }
33+
let condStr = String(parts[0])
34+
35+
switch condStr {
36+
case "SKC":
37+
parts.removeFirst()
38+
return [.skyClear]
39+
case "CLR", "NCD":
40+
parts.removeFirst()
41+
return [.clear]
42+
case "NSC":
43+
parts.removeFirst()
44+
return [.noSignificantClouds]
45+
default:
46+
break
5147
}
52-
53-
switch coverage {
54-
case "FEW": conditions.append(.few(height, type: type))
55-
case "SCT": conditions.append(.scattered(height, type: type))
56-
case "BKN": conditions.append(.broken(height, type: type))
57-
case "OVC": conditions.append(.overcast(height, type: type))
58-
case "VV": conditions.append(.indefinite(height))
59-
default: throw Error.invalidConditions(condStr)
48+
49+
if let match = try rx.wholeMatch(in: condStr) {
50+
parts.removeFirst()
51+
52+
let coverage = match[coverageRef],
53+
height = match[heightRef],
54+
type = match[ceilingTypeRef]
55+
56+
if type != nil {
57+
guard coverage != .verticalVis else { throw Error.invalidConditions(condStr) }
58+
}
59+
60+
switch coverage {
61+
case .few: conditions.append(.few(height, type: type))
62+
case .scattered: conditions.append(.scattered(height, type: type))
63+
case .broken: conditions.append(.broken(height, type: type))
64+
case .overcast: conditions.append(.overcast(height, type: type))
65+
case .verticalVis: conditions.append(.indefinite(height))
66+
}
6067
}
68+
else { return conditions }
6169
}
62-
else { return conditions }
6370
}
6471
}

0 commit comments

Comments
 (0)