diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 947f02a..7025f4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,16 +2,14 @@ # # Category A (tools-version 6.2 + macOS 26 min): macos-26 + Swift 6.2 # Category B (tools-version 6.2 + older macOS): macos-15 + 6.2, macos-26 + 6.2 -# Category C (tools-version 6.3): macos-15 + 6.3, macos-26 + 6.3 -# Linux: ubuntu + Swift 6.3 +# Category C (tools-version 6.3): macos-15 + 6.3/6.4, macos-26 + 6.3/6.4 +# Linux: ubuntu + Swift 6.3/6.4 # (Package.swift excludes METARFormatting/DecodeMETAR/DecodeTAF on Linux — they format # Measurement values via APIs swift-corelibs-foundation doesn't implement; SwiftMETAR # core + DecodeWindsAloft + tests are unaffected and build/test on Linux normally) # # This package is Category C. BuildableMacro requires tools-version 6.3, so Swift # 6.1 and 6.2 can no longer resolve it. -# -# When Swift 6.4 ships: add 6.4 legs alongside 6.3 name: CI @@ -34,10 +32,16 @@ jobs: include: - os: macos-15 swift: "6.3" + - os: macos-15 + swift: "6.4" - os: macos-26 swift: "6.3" + - os: macos-26 + swift: "6.4" - os: ubuntu-latest swift: "6.3" + - os: ubuntu-latest + swift: "6.4" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5edf247..5edd2e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Change Log +## [Unreleased] + +### Changed + +- Every throwing API now declares its error type: `throws(Error)` replaces + untyped `throws` across the parsers and on public entry points such as + `METAR.from(string:on:)`, `TAF.from(string:on:)`, and + `CodedRepresentable.init(coded:)`. Callers need no changes — `catch` blocks + that matched `SwiftMETAR.Error` keep working, and exhaustive `catch` over the + enum's cases is now possible without a fallback clause. +- Case-set regexes (`Weather.Phenomenon`, `Remark.Coverage`, and the other + `RawRepresentable` code sets) are built with RegexBuilder and cached per type + instead of being assembled as a pattern string and recompiled on every access. +- ISO 8601 timestamps in aviationweather.gov XML are parsed with + `Date.ISO8601FormatStyle` rather than `ISO8601DateFormatter`. + ## [4.1.0] - 2026-09-14 ### Changed diff --git a/Package.swift b/Package.swift index 8066a66..de41434 100644 --- a/Package.swift +++ b/Package.swift @@ -8,7 +8,8 @@ let upcomingFeatures: [SwiftSetting] = [ .enableUpcomingFeature("ImmutableWeakCaptures"), .enableUpcomingFeature("MemberImportVisibility"), .enableUpcomingFeature("ExistentialAny"), - .enableUpcomingFeature("InternalImportsByDefault") + .enableUpcomingFeature("InternalImportsByDefault"), + .strictMemorySafety() ] // METARFormatting (and DecodeMETAR/DecodeTAF, which depend on it) format `Measurement` diff --git a/Sources/SwiftMETAR/Common/Altimeter.swift b/Sources/SwiftMETAR/Common/Altimeter.swift index 2ed45db..b8d8e0f 100644 --- a/Sources/SwiftMETAR/Common/Altimeter.swift +++ b/Sources/SwiftMETAR/Common/Altimeter.swift @@ -28,8 +28,8 @@ public enum Altimeter: CodedRepresentable, Comparable, Sendable { */ public var codedString: String { switch self { - case .inHg(let value): "A\(String(format: "%04d", value))" - case .hPa(let value): "Q\(String(format: "%04d", value))" + case .inHg(let value): "A\(unsafe String(format: "%04d", value))" + case .hPa(let value): "Q\(unsafe String(format: "%04d", value))" } } @@ -40,7 +40,7 @@ public enum Altimeter: CodedRepresentable, Comparable, Sendable { - Throws: ``Error/invalidAltimeter(_:)`` if `coded` is not a valid altimeter setting. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { var parts = coded.split(whereSeparator: \.isWhitespace) guard let altimeter = try AltimeterParser().parseMETAR(&parts), parts.isEmpty else { throw Error.invalidAltimeter(coded) diff --git a/Sources/SwiftMETAR/Common/CodedRepresentable.swift b/Sources/SwiftMETAR/Common/CodedRepresentable.swift index 55fdc45..94fe7b2 100644 --- a/Sources/SwiftMETAR/Common/CodedRepresentable.swift +++ b/Sources/SwiftMETAR/Common/CodedRepresentable.swift @@ -22,7 +22,7 @@ public protocol CodedRepresentable: Codable { - Parameter coded: The coded string, e.g. `"03015KT"`. - Throws: An ``Error`` if `coded` is not a valid representation. */ - init(coded: String) throws + init(coded: String) throws(Error) } extension CodedRepresentable { diff --git a/Sources/SwiftMETAR/Common/Condition.swift b/Sources/SwiftMETAR/Common/Condition.swift index 390aeed..8e0ba94 100644 --- a/Sources/SwiftMETAR/Common/Condition.swift +++ b/Sources/SwiftMETAR/Common/Condition.swift @@ -108,7 +108,7 @@ public enum Condition: CodedRepresentable, Equatable, Sendable { - Throws: ``Error/invalidConditions(_:)`` if `coded` is not a valid representation of a single sky condition. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { if coded == "CAVOK" { self = .cavok return @@ -123,7 +123,7 @@ public enum Condition: CodedRepresentable, Equatable, Sendable { } private static func heightGroup(_ feet: UInt) -> String { - String(format: "%03d", feet / 100) + unsafe String(format: "%03d", feet / 100) } private static func suffix(_ type: CeilingType?) -> String { diff --git a/Sources/SwiftMETAR/Common/Parsers/CommonParsers.swift b/Sources/SwiftMETAR/Common/Parsers/CommonParsers.swift index 1011325..31185b2 100644 --- a/Sources/SwiftMETAR/Common/Parsers/CommonParsers.swift +++ b/Sources/SwiftMETAR/Common/Parsers/CommonParsers.swift @@ -1,4 +1,4 @@ -func parseLocationID(_ parts: inout [String.SubSequence]) throws -> String { +func parseLocationID(_ parts: inout [String.SubSequence]) throws(Error) -> String { guard !parts.isEmpty else { throw Error.badFormat } return String(parts.removeFirst()) } diff --git a/Sources/SwiftMETAR/Common/Parsers/ConditionsParser.swift b/Sources/SwiftMETAR/Common/Parsers/ConditionsParser.swift index f73c8b6..23d8dab 100644 --- a/Sources/SwiftMETAR/Common/Parsers/ConditionsParser.swift +++ b/Sources/SwiftMETAR/Common/Parsers/ConditionsParser.swift @@ -5,12 +5,11 @@ final class ConditionsParser: WarmableParser, @unchecked Sendable { private static let coverageRef = Reference() private static let heightRef = Reference() private static let ceilingTypeRef = Reference() - // swiftlint:disable force_try private static let rx = LockedRegex( Regex { Anchor.startOfSubject Capture(as: coverageRef) { - try! Coverage.rx + Coverage.rx } transform: { .init(rawValue: String($0))! } @@ -20,16 +19,15 @@ final class ConditionsParser: WarmableParser, @unchecked Sendable { .init($0)! * 100 } Capture(as: ceilingTypeRef) { - Optionally(try! Condition.CeilingType.rx) + Optionally(Condition.CeilingType.rx) } transform: { .init(rawValue: String($0)) } Anchor.endOfSubject } ) - // swiftlint:enable force_try - func parse(_ parts: inout [String.SubSequence]) throws -> [Condition] { + func parse(_ parts: inout [String.SubSequence]) throws(Error) -> [Condition] { if parts.isEmpty { return [] } var conditions = [Condition]() diff --git a/Sources/SwiftMETAR/Common/Parsers/DateParser.swift b/Sources/SwiftMETAR/Common/Parsers/DateParser.swift index dbeac86..7fc570f 100644 --- a/Sources/SwiftMETAR/Common/Parsers/DateParser.swift +++ b/Sources/SwiftMETAR/Common/Parsers/DateParser.swift @@ -36,7 +36,7 @@ final class DayHourMinuteParser: WarmableParser, @unchecked Sendable { _ = try? anchoredRx.wholeMatch(in: "") } - func parse(_ parts: inout [String.SubSequence], referenceDate: Date? = nil) throws + func parse(_ parts: inout [String.SubSequence], referenceDate: Date? = nil) throws(Error) -> DateComponents { guard !parts.isEmpty else { throw Error.badFormat } @@ -48,12 +48,13 @@ final class DayHourMinuteParser: WarmableParser, @unchecked Sendable { return try parse(match: match, referenceDate: referenceDate, originalString: dateStr) } - func matchesNext(_ parts: [String.SubSequence]) throws -> Bool { + func matchesNext(_ parts: [String.SubSequence]) throws(Error) -> Bool { guard let str = parts.first else { return false } return try anchoredRx.matches(String(str)) } - func parse(match: Regex.Match, referenceDate: Date? = nil, originalString: String) throws + func parse(match: Regex.Match, referenceDate: Date? = nil, originalString: String) + throws(Error) -> DateComponents { var (day, hour, minute) = match[dateRef] @@ -109,7 +110,7 @@ class DayHourParser { referenceDate: Date? = nil, afterDate afterDateComponents: DateComponents? = nil, originalString string: String - ) throws -> DateComponents { + ) throws(Error) -> DateComponents { var (day, hour) = match[dateRef] var addDay = false if hour == 24 { @@ -198,7 +199,7 @@ class HourMinutePeriodParser { match: Regex.Match, referenceDate: Date? = nil, originalString: String - ) throws -> DateComponentsInterval { + ) throws(Error) -> DateComponentsInterval { let startTime = try minuteOfDay( hour: match[startHourRef], minute: match[startMinuteRef], @@ -227,7 +228,8 @@ class HourMinutePeriodParser { /// Converts an `HHMM` time to minutes past midnight, treating `2400` as the end /// of the day the way ``DayHourMinuteParser`` does. - private func minuteOfDay(hour: UInt8, minute: UInt8, originalString: String) throws -> Int { + private func minuteOfDay(hour: UInt8, minute: UInt8, originalString: String) throws(Error) -> Int + { guard hour <= 24, minute <= 59, hour < 24 || minute == 0 else { throw Error.invalidDate(originalString) } @@ -276,7 +278,7 @@ class HourMinuteParser { } func parse(match: Regex.Match, referenceDate: Date? = nil, originalString string: String) - throws -> DateComponents + throws(Error) -> DateComponents { let (hour, minute) = match[dateRef] diff --git a/Sources/SwiftMETAR/Common/Parsers/VisibilityParser.swift b/Sources/SwiftMETAR/Common/Parsers/VisibilityParser.swift index ae8c3c5..fac2698 100644 --- a/Sources/SwiftMETAR/Common/Parsers/VisibilityParser.swift +++ b/Sources/SwiftMETAR/Common/Parsers/VisibilityParser.swift @@ -41,7 +41,7 @@ final class VisibilityParser: WarmableParser, @unchecked Sendable { _ = try? fractionRx.wholeMatch(in: "") } - func parse(_ parts: inout [String.SubSequence]) throws -> Visibility? { + func parse(_ parts: inout [String.SubSequence]) throws(Error) -> Visibility? { guard !parts.isEmpty else { return nil } if parts.count >= 2 { @@ -59,7 +59,7 @@ final class VisibilityParser: WarmableParser, @unchecked Sendable { return nil } - private func parse(_ vizStr: String) throws -> Visibility? { + private func parse(_ vizStr: String) throws(Error) -> Visibility? { if vizStr == "CAVOK" { return .greaterThan(.meters(9999)) } @@ -94,15 +94,13 @@ final class VisibilityParser: WarmableParser, @unchecked Sendable { class OpenRangeParser { private static let boundRef = Reference() - // swiftlint:disable force_try static let rx = Regex { Capture(as: boundRef) { - try! OpenRange.rx + OpenRange.rx } transform: { .init(rawValue: String($0))! } } - // swiftlint:enable force_try func parse(_ match: Regex.Match) -> OpenRange { match[Self.boundRef] @@ -124,7 +122,6 @@ final class VisibilityParser: WarmableParser, @unchecked Sendable { private static let valueRef = Reference() private static let unitRef = Reference() - // swiftlint:disable force_try static let rx = Regex { OpenRangeParser.rx Capture(as: valueRef) { @@ -133,12 +130,11 @@ final class VisibilityParser: WarmableParser, @unchecked Sendable { .init($0)! } Capture(as: unitRef) { - try! Optionally(VisibilityDistanceUnit.rx) + Optionally(VisibilityDistanceUnit.rx) } transform: { .from(raw: String($0))! } } - // swiftlint:enable force_try private let openRangeParser = OpenRangeParser() diff --git a/Sources/SwiftMETAR/Common/Parsers/WeatherParser.swift b/Sources/SwiftMETAR/Common/Parsers/WeatherParser.swift index 16314dd..e49677c 100644 --- a/Sources/SwiftMETAR/Common/Parsers/WeatherParser.swift +++ b/Sources/SwiftMETAR/Common/Parsers/WeatherParser.swift @@ -6,22 +6,21 @@ final class WeatherParser: WarmableParser, @unchecked Sendable { private static let descriptorRef = Reference() private static let phenomenaRef = Reference() - // swiftlint:disable force_try private static let weatherRx = LockedRegex( Regex { Anchor.startOfSubject Capture(as: intensityRef) { - try! Weather.Intensity.rx + Weather.Intensity.rx } transform: { .init(rawValue: String($0))! } Capture(as: descriptorRef) { - Optionally { try! Weather.Descriptor.rx } + Optionally { Weather.Descriptor.rx } } transform: { .init(rawValue: String($0)) } Capture(as: phenomenaRef) { - OneOrMore { try! Weather.Phenomenon.rx } + OneOrMore { Weather.Phenomenon.rx } } Anchor.endOfSubject } @@ -33,9 +32,8 @@ final class WeatherParser: WarmableParser, @unchecked Sendable { Anchor.endOfSubject } ) - // swiftlint:enable force_try - func parse(_ parts: inout [String.SubSequence]) throws -> [Weather]? { + func parse(_ parts: inout [String.SubSequence]) throws(Error) -> [Weather]? { var weather = [Weather]() while true { @@ -62,17 +60,16 @@ final class WeatherParser: WarmableParser, @unchecked Sendable { let intensity = match[Self.intensityRef] let descriptor = match[Self.descriptorRef] - let phenomenaStr = match[Self.phenomenaRef] - let phenomenaStrs = String(phenomenaStr).partition(by: 2) - let phenomena = try phenomenaStrs.map { code -> Weather.Phenomenon in + var phenomena = Set() + for code in String(match[Self.phenomenaRef]).partition(by: 2) { guard let phenomenon = Weather.Phenomenon(rawValue: code) else { throw Error.invalidWeather(weatherStr) } - return phenomenon + phenomena.insert(phenomenon) } weather.append( - Weather(intensity: intensity, descriptor: descriptor, phenomena: Set(phenomena)) + Weather(intensity: intensity, descriptor: descriptor, phenomena: phenomena) ) } else { return weather diff --git a/Sources/SwiftMETAR/Common/Parsers/WindParser.swift b/Sources/SwiftMETAR/Common/Parsers/WindParser.swift index 3585870..c6fb943 100644 --- a/Sources/SwiftMETAR/Common/Parsers/WindParser.swift +++ b/Sources/SwiftMETAR/Common/Parsers/WindParser.swift @@ -70,7 +70,7 @@ enum WindParser { } ) - static func parse(_ parts: inout [String.SubSequence]) throws -> Wind? { + static func parse(_ parts: inout [String.SubSequence]) throws(Error) -> Wind? { guard !parts.isEmpty else { return nil } let dirAndSpeed = String(parts[0]) @@ -122,7 +122,7 @@ enum WindParser { } } - static func parse(match: Regex.Match, originalString: String) throws -> Wind { + static func parse(match: Regex.Match, originalString: String) throws(Error) -> Wind { let speedValue = match[speedRef] let speed: Wind.Speed = switch match[unitRef] { @@ -155,7 +155,7 @@ enum WindParser { private static func parseDirectionRange( _ parts: inout [String.SubSequence], rangeSeq: String.SubSequence - ) throws -> (UInt16, UInt16)? { + ) throws(Error) -> (UInt16, UInt16)? { let rangeStr = String(rangeSeq) guard let variableMatch = try variableRx.wholeMatch(in: rangeStr), diff --git a/Sources/SwiftMETAR/Common/Parsers/WindshearParser.swift b/Sources/SwiftMETAR/Common/Parsers/WindshearParser.swift index b87dfa0..e97c7de 100644 --- a/Sources/SwiftMETAR/Common/Parsers/WindshearParser.swift +++ b/Sources/SwiftMETAR/Common/Parsers/WindshearParser.swift @@ -18,7 +18,7 @@ final class WindshearParser: WarmableParser, @unchecked Sendable { } ) - func parse(_ parts: inout [String.SubSequence]) throws -> Windshear? { + func parse(_ parts: inout [String.SubSequence]) throws(Error) -> Windshear? { guard !parts.isEmpty else { return nil } let windshearStr = String(parts[0]) guard let result = try Self.rx.wholeMatch(in: windshearStr) else { return nil } diff --git a/Sources/SwiftMETAR/Common/Visibility.swift b/Sources/SwiftMETAR/Common/Visibility.swift index 1c756ae..124951b 100644 --- a/Sources/SwiftMETAR/Common/Visibility.swift +++ b/Sources/SwiftMETAR/Common/Visibility.swift @@ -65,7 +65,7 @@ public enum Visibility: CodedRepresentable, Equatable, Sendable { - Throws: ``Error/invalidVisibility(_:)`` if `coded` is not a valid visibility group. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { var parts = coded.split(whereSeparator: \.isWhitespace) if let visibility = try VisibilityParser().parse(&parts), parts.isEmpty { self = visibility @@ -172,7 +172,7 @@ public enum Visibility: CodedRepresentable, Equatable, Sendable { - Throws: ``Error/invalidVisibility(_:)`` if `coded` is not a valid, unqualified distance. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { guard case let .equal(value) = try Visibility(coded: coded) else { throw Error.invalidVisibility(coded) } diff --git a/Sources/SwiftMETAR/Common/Weather.swift b/Sources/SwiftMETAR/Common/Weather.swift index e6e8234..e81cb55 100644 --- a/Sources/SwiftMETAR/Common/Weather.swift +++ b/Sources/SwiftMETAR/Common/Weather.swift @@ -205,7 +205,7 @@ extension Weather: CodedRepresentable { - Throws: ``Error/invalidWeather(_:)`` if `coded` is not a single valid weather group. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { var parts = coded.split(whereSeparator: \.isWhitespace) guard let weather = try WeatherParser().parse(&parts), weather.count == 1, parts.isEmpty diff --git a/Sources/SwiftMETAR/Common/Wind.swift b/Sources/SwiftMETAR/Common/Wind.swift index 1a47e91..c7f1d00 100644 --- a/Sources/SwiftMETAR/Common/Wind.swift +++ b/Sources/SwiftMETAR/Common/Wind.swift @@ -63,7 +63,7 @@ public enum Wind: CodedRepresentable, Equatable, Sendable { } } - public init(coded: String) throws { + public init(coded: String) throws(Error) { var parts = coded.split(whereSeparator: \.isWhitespace) guard let wind = try WindParser.parse(&parts), parts.isEmpty else { throw Error.invalidWinds(coded) @@ -72,7 +72,7 @@ public enum Wind: CodedRepresentable, Equatable, Sendable { } private static func heading(_ value: UInt16) -> String { - String(format: "%03d", value) + unsafe String(format: "%03d", value) } private static func speedGroup(_ speed: Speed, gust: Speed?) -> String { @@ -150,13 +150,13 @@ public enum Wind: CodedRepresentable, Equatable, Sendable { switch self { case .knots(let quantity), .kph(let quantity), .mps(let quantity): quantity } - return String(format: "%02d", quantity) + return unsafe String(format: "%02d", quantity) } /// The coded representation of this speed, e.g. `"15KT"`. public var codedString: String { "\(speedDigits)\(unitCode)" } - public init(coded: String) throws { + public init(coded: String) throws(Error) { let units: [(suffix: String, make: (UInt16) -> Self)] = [ ("KTS", Self.knots), ("KT", Self.knots), ("MPS", Self.mps), ("KPH", Self.kph) ] diff --git a/Sources/SwiftMETAR/Common/Windshear.swift b/Sources/SwiftMETAR/Common/Windshear.swift index 0c68a32..e0ea5b2 100644 --- a/Sources/SwiftMETAR/Common/Windshear.swift +++ b/Sources/SwiftMETAR/Common/Windshear.swift @@ -20,7 +20,7 @@ public struct Windshear: CodedRepresentable, Equatable, Sendable { by the wind's ``Wind/codedString``. */ public var codedString: String { - "WS\(String(format: "%03d", height / 100))/\(wind.codedString)" + "WS\(unsafe String(format: "%03d", height / 100))/\(wind.codedString)" } /** @@ -30,7 +30,7 @@ public struct Windshear: CodedRepresentable, Equatable, Sendable { - Throws: ``Error/invalidWindshear(_:)`` if `coded` is not a valid windshear representation. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { var parts = coded.split(whereSeparator: \.isWhitespace) guard let windshear = try WindshearParser().parse(&parts), parts.isEmpty else { throw Error.invalidWindshear(coded) diff --git a/Sources/SwiftMETAR/METAR/METAR+Coded.swift b/Sources/SwiftMETAR/METAR/METAR+Coded.swift index cd70ac6..a4df3c5 100644 --- a/Sources/SwiftMETAR/METAR/METAR+Coded.swift +++ b/Sources/SwiftMETAR/METAR/METAR+Coded.swift @@ -49,12 +49,12 @@ extension METAR: CodedRepresentable { - Parameter coded: The coded METAR string. - Throws: An ``Error`` if `coded` is not a valid METAR. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { self = try METARParser.parseSynchronously(coded) } static func zuluTimestamp(_ components: DateComponents) -> String { - String( + unsafe String( format: "%02d%02d%02dZ", components.day ?? 0, components.hour ?? 0, @@ -68,7 +68,7 @@ extension METAR: CodedRepresentable { private static func codedTemperature(_ value: Int8?) -> String { guard let value else { return "" } - let magnitude = String(format: "%02d", abs(Int(value))) + let magnitude = unsafe String(format: "%02d", abs(Int(value))) return value < 0 ? "M\(magnitude)" : magnitude } } diff --git a/Sources/SwiftMETAR/METAR/METAR.swift b/Sources/SwiftMETAR/METAR/METAR.swift index 26dce00..3ece5d6 100644 --- a/Sources/SwiftMETAR/METAR/METAR.swift +++ b/Sources/SwiftMETAR/METAR/METAR.swift @@ -92,7 +92,7 @@ public struct METAR: Sendable { - Throws: If a parsing error occurs. */ public static func from(string: String, on date: Date? = nil, lenientRemarks: Bool = false) - async throws -> Self + async throws(Error) -> Self { return try await METARParser.shared.parse(string, on: date, lenientRemarks: lenientRemarks) } diff --git a/Sources/SwiftMETAR/METAR/Parsers/AltimeterParser.swift b/Sources/SwiftMETAR/METAR/Parsers/AltimeterParser.swift index 9f47bcb..9b81401 100644 --- a/Sources/SwiftMETAR/METAR/Parsers/AltimeterParser.swift +++ b/Sources/SwiftMETAR/METAR/Parsers/AltimeterParser.swift @@ -36,7 +36,7 @@ final class AltimeterParser: WarmableParser, @unchecked Sendable { } ) - func parseMETAR(_ parts: inout [String.SubSequence]) throws -> Altimeter? { + func parseMETAR(_ parts: inout [String.SubSequence]) throws(Error) -> Altimeter? { guard !parts.isEmpty else { return nil } let altStr = String(parts[0]) @@ -52,7 +52,7 @@ final class AltimeterParser: WarmableParser, @unchecked Sendable { } } - func parseTAF(_ parts: inout [String.SubSequence]) throws -> Altimeter? { + func parseTAF(_ parts: inout [String.SubSequence]) throws(Error) -> Altimeter? { guard !parts.isEmpty else { return nil } let altStr = String(parts[0]) diff --git a/Sources/SwiftMETAR/METAR/Parsers/METARParser.swift b/Sources/SwiftMETAR/METAR/Parsers/METARParser.swift index 25e0a68..324b0e0 100644 --- a/Sources/SwiftMETAR/METAR/Parsers/METARParser.swift +++ b/Sources/SwiftMETAR/METAR/Parsers/METARParser.swift @@ -19,9 +19,12 @@ actor METARParser { _ codedMETAR: String, on referenceDate: Date? = nil, lenientRemarks: Bool = false - ) throws -> METAR { + ) throws(Error) -> METAR { let remarkParsers = RemarksParser.sharedParsers - return try assemble(codedMETAR, referenceDate: referenceDate) { parts, date in + func parseRemarks( + _ parts: inout [Substring], + _ date: DateComponents + ) throws(Error) -> ([RemarkEntry], String?) { try RemarksParser.parse( &parts, using: remarkParsers, @@ -29,6 +32,8 @@ actor METARParser { lenientRemarks: lenientRemarks ) } + + return try assemble(codedMETAR, referenceDate: referenceDate, parseRemarks: parseRemarks) } /// The isolation-free parsing core, shared by the async and synchronous paths. @@ -37,16 +42,16 @@ actor METARParser { private static func assemble( _ codedMETAR: String, referenceDate: Date?, - parseRemarks: (_ parts: inout [Substring], _ date: DateComponents) throws -> ( + parseRemarks: (_ parts: inout [Substring], _ date: DateComponents) throws(Error) -> ( [RemarkEntry], String? ) - ) throws -> METAR { + ) throws(Error) -> METAR { var parts = codedMETAR.split(separator: .whitespacesAndNewlines) let issuance = try parseIssuance(&parts) let stationID = try parseLocationID(&parts) let date = try dateParser.parse(&parts, referenceDate: referenceDate) let observer = try parseObserver(&parts) - let wind = try orMissing(&parts, defaultValue: nil) { try WindParser.parse(&$0) } + let wind = try orMissing(&parts, defaultValue: nil, WindParser.parse) let visibility: Visibility? let runwayViz: [RunwayVisibility] @@ -59,15 +64,13 @@ actor METARParser { weather = [] conditions = [.cavok] } else { - visibility = try orMissing(&parts, defaultValue: nil) { try visibilityParser.parse(&$0) } + visibility = try orMissing(&parts, defaultValue: nil, visibilityParser.parse) runwayViz = try rvrParser.parse(&parts) - weather = try orMissing(&parts, defaultValue: nil) { try weatherParser.parse(&$0) } - conditions = try orMissing(&parts, defaultValue: []) { try conditionsParser.parse(&$0) } - } - let tempDewpoint = try orMissing(&parts, defaultValue: (nil, nil)) { - try temperatureParser.parse(&$0) + weather = try orMissing(&parts, defaultValue: nil, weatherParser.parse) + conditions = try orMissing(&parts, defaultValue: [], conditionsParser.parse) } - let altimeter = try orMissing(&parts, defaultValue: nil) { try altimeterParser.parseMETAR(&$0) } + let tempDewpoint = try orMissing(&parts, defaultValue: (nil, nil), temperatureParser.parse) + let altimeter = try orMissing(&parts, defaultValue: nil, altimeterParser.parseMETAR) let (remarks, remarksString) = try parseRemarks(&parts, date) return METAR( @@ -89,7 +92,9 @@ actor METARParser { ) } - private static func parseIssuance(_ parts: inout [String.SubSequence]) throws -> METAR.Issuance { + private static func parseIssuance(_ parts: inout [String.SubSequence]) throws(Error) + -> METAR.Issuance + { guard !parts.isEmpty else { throw Error.badFormat } let typeCode = String(parts[0]) @@ -100,7 +105,9 @@ actor METARParser { return type } - private static func parseObserver(_ parts: inout [String.SubSequence]) throws -> METAR.Observer { + private static func parseObserver(_ parts: inout [String.SubSequence]) throws(Error) + -> METAR.Observer + { guard !parts.isEmpty else { throw Error.badFormat } let observer = METAR.Observer(rawValue: String(parts[0])) @@ -111,8 +118,8 @@ actor METARParser { private static func orMissing( _ parts: inout [Substring], defaultValue: T, - _ parser: (_ parts: inout [Substring]) throws -> T - ) rethrows -> T { + _ parser: (_ parts: inout [Substring]) throws(Error) -> T + ) throws(Error) -> T { if parts.first == "M" { parts.removeFirst() return defaultValue @@ -121,9 +128,12 @@ actor METARParser { } func parse(_ codedMETAR: String, on referenceDate: Date? = nil, lenientRemarks: Bool = false) - throws -> METAR + throws(Error) -> METAR { - try Self.assemble(codedMETAR, referenceDate: referenceDate) { parts, date in + func parseRemarks( + _ parts: inout [Substring], + _ date: DateComponents + ) throws(Error) -> ([RemarkEntry], String?) { try RemarksParser.parse( &parts, using: RemarksParser.sharedParsers, @@ -131,5 +141,11 @@ actor METARParser { lenientRemarks: lenientRemarks ) } + + return try Self.assemble( + codedMETAR, + referenceDate: referenceDate, + parseRemarks: parseRemarks + ) } } diff --git a/Sources/SwiftMETAR/METAR/Parsers/METARTemperatureParser.swift b/Sources/SwiftMETAR/METAR/Parsers/METARTemperatureParser.swift index 3a08335..c6a37ba 100644 --- a/Sources/SwiftMETAR/METAR/Parsers/METARTemperatureParser.swift +++ b/Sources/SwiftMETAR/METAR/Parsers/METARTemperatureParser.swift @@ -18,7 +18,7 @@ final class METARTemperatureParser: WarmableParser, @unchecked Sendable { _ = try? rx.wholeMatch(in: "") } - func parse(_ parts: inout [String.SubSequence]) throws -> (Int8?, Int8?) { + func parse(_ parts: inout [String.SubSequence]) throws(Error) -> (Int8?, Int8?) { if parts.isEmpty { return (nil, nil) } let tempStr = String(parts[0]) diff --git a/Sources/SwiftMETAR/METAR/Parsers/METARXMLParser.swift b/Sources/SwiftMETAR/METAR/Parsers/METARXMLParser.swift index 211c89f..1b61046 100644 --- a/Sources/SwiftMETAR/METAR/Parsers/METARXMLParser.swift +++ b/Sources/SwiftMETAR/METAR/Parsers/METARXMLParser.swift @@ -13,7 +13,7 @@ actor METARXMLParser { // MARK: - Type Methods - private static func buildMETAR(from entry: Entry) throws -> METAR { + private static func buildMETAR(from entry: Entry) throws(Error) -> METAR { guard let stationID = entry.stationID else { throw Error.badFormat } @@ -86,7 +86,9 @@ actor METARXMLParser { func parse(data: Data) -> [XMLParseResult] { let delegate = XMLDelegate() let xmlParser = XMLParser(data: data) - xmlParser.delegate = delegate + // `XMLParser.delegate` is `unowned(unsafe)`; `delegate` is held strongly by this + // frame for the whole parse and is read again afterwards, so it outlives the parser. + unsafe xmlParser.delegate = delegate xmlParser.parse() return delegate.entries.map { entry in diff --git a/Sources/SwiftMETAR/METAR/Parsers/RVRParser.swift b/Sources/SwiftMETAR/METAR/Parsers/RVRParser.swift index 430f0ce..8b2cbc4 100644 --- a/Sources/SwiftMETAR/METAR/Parsers/RVRParser.swift +++ b/Sources/SwiftMETAR/METAR/Parsers/RVRParser.swift @@ -92,7 +92,7 @@ final class RVRParser: WarmableParser, @unchecked Sendable { } ) - func parse(_ parts: inout [String.SubSequence]) throws -> [RunwayVisibility] { + func parse(_ parts: inout [String.SubSequence]) throws(Error) -> [RunwayVisibility] { var visibilities = [RunwayVisibility]() while true { diff --git a/Sources/SwiftMETAR/METAR/RunwayVisibility.swift b/Sources/SwiftMETAR/METAR/RunwayVisibility.swift index 97c25dd..6555391 100644 --- a/Sources/SwiftMETAR/METAR/RunwayVisibility.swift +++ b/Sources/SwiftMETAR/METAR/RunwayVisibility.swift @@ -30,7 +30,7 @@ extension RunwayVisibility: CodedRepresentable { - Parameter coded: The coded RVR string, which must contain exactly one group. - Throws: ``Error/badFormat`` if `coded` is not a single valid RVR group. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { var parts = coded.split(whereSeparator: \.isWhitespace) let visibilities = try RVRParser().parse(&parts) guard visibilities.count == 1, parts.isEmpty else { throw Error.badFormat } diff --git a/Sources/SwiftMETAR/Remarks/Parsers/RemarkDirectionParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/RemarkDirectionParser.swift index b409d83..97a9eaa 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/RemarkDirectionParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/RemarkDirectionParser.swift @@ -14,26 +14,22 @@ class RemarkDirectionParser { "ALQS": .all, "ALQDS": .all ] - /// Alternatives ordered longest-first. - /// - /// Alternation matches leftmost-first, so a shorter alternative that prefixes a longer one wins - /// and truncates the match — `N` would claim the `N` of `NE`. Length order also makes the pattern - /// stable: `keys` has no guaranteed order, and Swift seeds hashing per process, so joining it - /// directly built a different regex from run to run. - private static let directionAlternatives = directionFromString.keys - .sorted { ($0.count, $1) > ($1.count, $0) } - .joined(separator: "|") + + /// Longest first, so that a longer abbreviation is never shadowed by a shorter one it + /// starts with (`NE` by `N`, `ALQDS` by `ALQS`). `Dictionary.keys` has no guaranteed + /// order, so the alternatives are sorted into a deterministic one. + private static let directionAlternatives = directionFromString.keys.sorted { + ($1.count, $0) < ($0.count, $1) + } private let directionRef = Reference() - // swiftlint:disable force_try lazy var rx = Regex { Capture(as: directionRef) { - try! Regex("(?:\(RemarkDirectionParser.directionAlternatives))") + regexAlternation(of: RemarkDirectionParser.directionAlternatives) } transform: { RemarkDirectionParser.directionFromString[String($0)] } } - // swiftlint:enable force_try static func isDirectionString(_ string: String) -> Bool { Self.directionFromString.keys.contains(string) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/RemarkParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/RemarkParser.swift index 512a319..f82538c 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/RemarkParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/RemarkParser.swift @@ -6,7 +6,7 @@ protocol RemarkParser: Sendable { var urgency: Remark.Urgency { get } init() - func parse(remarks: inout String, date: DateComponents) throws -> Remark? + func parse(remarks: inout String, date: DateComponents) throws(Error) -> Remark? } extension RemarkParser { diff --git a/Sources/SwiftMETAR/Remarks/Parsers/RemarksParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/RemarksParser.swift index 27fd00b..963cf38 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/RemarksParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/RemarksParser.swift @@ -48,7 +48,7 @@ enum RemarksParser { using parsers: [any RemarkParser], date: DateComponents, lenientRemarks: Bool = false - ) throws -> ([RemarkEntry], String?) { + ) throws(Error) -> ([RemarkEntry], String?) { if parts.isEmpty { return ([], nil) } if parts.count == 1 && parts[0].isEmpty { return ([], nil) } // extra space after METAR diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/AircraftMishapParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/AircraftMishapParser.swift index 06a89ea..5484622 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/AircraftMishapParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/AircraftMishapParser.swift @@ -12,7 +12,7 @@ final class AircraftMishapParser: RemarkParser, @unchecked Sendable { var urgency = Remark.Urgency.urgent - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let match = try Self.rx.firstMatch(in: remarks) else { return nil } remarks.removeSubrange(match.range) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/CloudTypesParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/CloudTypesParser.swift index cf5e6fe..eb2303b 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/CloudTypesParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/CloudTypesParser.swift @@ -8,31 +8,29 @@ final class CloudTypesParser: RemarkParser, @unchecked Sendable { private let midRef = Reference() private let highRef = Reference() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary "8/" Capture(as: lowRef) { - try! Remark.LowCloudType.rx + Remark.LowCloudType.rx } transform: { .init(rawValue: String($0))! } Capture(as: midRef) { - try! Remark.MiddleCloudType.rx + Remark.MiddleCloudType.rx } transform: { .init(rawValue: String($0))! } Capture(as: highRef) { - try! Remark.HighCloudType.rx + Remark.HighCloudType.rx } transform: { .init(rawValue: String($0))! } } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let low = result[lowRef] let mid = result[midRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/CorrectionParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/CorrectionParser.swift index cb04425..ccca434 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/CorrectionParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/CorrectionParser.swift @@ -25,7 +25,7 @@ final class CorrectionParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let hour = result[hourRef] let minute = result[minuteRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/DailyPrecipitationAmountParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/DailyPrecipitationAmountParser.swift index beda588..0e4fb10 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/DailyPrecipitationAmountParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/DailyPrecipitationAmountParser.swift @@ -29,7 +29,7 @@ final class DailyPrecipitationAmountParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } if result[missingRef] == true { diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/DailyTemperatureExtremeParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/DailyTemperatureExtremeParser.swift index e15d179..9f1e1e2 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/DailyTemperatureExtremeParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/DailyTemperatureExtremeParser.swift @@ -17,7 +17,7 @@ final class DailyTemperatureExtremeParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks), let highNum = highParser.parse(result), let lowNum = lowParser.parse(result) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/HailstoneSizeParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/HailstoneSizeParser.swift index 6ade454..c50cc51 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/HailstoneSizeParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/HailstoneSizeParser.swift @@ -15,7 +15,7 @@ final class HailstoneSizeParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let size = sizeParser.parse(result) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/HourlyPrecipitationAmountParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/HourlyPrecipitationAmountParser.swift index 8d45cd6..5054acf 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/HourlyPrecipitationAmountParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/HourlyPrecipitationAmountParser.swift @@ -19,7 +19,7 @@ final class HourlyPrecipitationAmountParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let amount = result[amountRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/LastParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/LastParser.swift index cdc993b..4afb322 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/LastParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/LastParser.swift @@ -12,7 +12,7 @@ final class LastParser: RemarkParser, @unchecked Sendable { var urgency = Remark.Urgency.routine - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try Self.rx.firstMatch(in: remarks) else { return nil } remarks.removeSubrange(result.range) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/LightningParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/LightningParser.swift index 418ce0a..205392e 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/LightningParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/LightningParser.swift @@ -9,13 +9,12 @@ final class LightningParser: RemarkParser, @unchecked Sendable { private let typesRef = Reference() private let proximityRef = Reference() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary Optionally { Capture(as: frequencyRef) { - try! Remark.Frequency.rx + Remark.Frequency.rx } transform: { .init(rawValue: String($0)) } @@ -24,7 +23,7 @@ final class LightningParser: RemarkParser, @unchecked Sendable { "LTG" Optionally { Capture(as: typesRef) { - OneOrMore { try! Remark.LightningType.rx } + OneOrMore { Remark.LightningType.rx } } transform: { $0 } @@ -32,7 +31,7 @@ final class LightningParser: RemarkParser, @unchecked Sendable { Optionally { " " Capture(as: proximityRef) { - try! Remark.Proximity.rx + Remark.Proximity.rx } transform: { .init(rawValue: String($0)) } @@ -44,9 +43,8 @@ final class LightningParser: RemarkParser, @unchecked Sendable { Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let frequency = result[frequencyRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/MaintenanceParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/MaintenanceParser.swift index 5f3fc9e..a6109ee 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/MaintenanceParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/MaintenanceParser.swift @@ -22,7 +22,7 @@ final class MaintenanceParser: RemarkParser, @unchecked Sendable { var urgency = Remark.Urgency.routine - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try Self.rx.firstMatch(in: remarks) else { return nil } remarks.removeSubrange(result.range) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/NOSIGParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/NOSIGParser.swift index e374606..db7616b 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/NOSIGParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/NOSIGParser.swift @@ -12,7 +12,7 @@ final class NOSIGParser: RemarkParser, @unchecked Sendable { var urgency = Remark.Urgency.routine - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try Self.rx.firstMatch(in: remarks) else { return nil } remarks.removeSubrange(result.range) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/NavalForecasterParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/NavalForecasterParser.swift index 9440d64..b00cd90 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/NavalForecasterParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/NavalForecasterParser.swift @@ -7,13 +7,12 @@ final class NavalForecasterParser: RemarkParser, @unchecked Sendable { private let centerRef = Reference() private let forecasterRef = Reference() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary "F" Capture(as: centerRef) { - try! Remark.NavalWeatherCenter.rx + Remark.NavalWeatherCenter.rx } transform: { .init(rawValue: String($0))! } @@ -25,9 +24,8 @@ final class NavalForecasterParser: RemarkParser, @unchecked Sendable { Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let center = result[centerRef] let forecaster = result[forecasterRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/NextParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/NextParser.swift index 738e8a7..7517c4e 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/NextParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/NextParser.swift @@ -15,7 +15,7 @@ final class NextParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let originalString = String(remarks[result.range]) let afterDate = try timeParser.parse(match: result, originalString: originalString) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/NoAmendmentsAfterParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/NoAmendmentsAfterParser.swift index 8088f5a..1e76ee4 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/NoAmendmentsAfterParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/NoAmendmentsAfterParser.swift @@ -17,7 +17,7 @@ final class NoAmendmentsAfterParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let originalString = String(remarks[result.range]) let afterDate = try timeParser.parse(match: result, originalString: originalString) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/NoSPECIParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/NoSPECIParser.swift index 704b2b8..fb40bd8 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/NoSPECIParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/NoSPECIParser.swift @@ -12,7 +12,7 @@ final class NoSPECIParser: RemarkParser, @unchecked Sendable { var urgency = Remark.Urgency.routine - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try Self.rx.firstMatch(in: remarks) else { return nil } remarks.removeSubrange(result.range) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/ObscurationParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/ObscurationParser.swift index 80cb12e..201734f 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/ObscurationParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/ObscurationParser.swift @@ -8,19 +8,18 @@ final class ObscurationParser: RemarkParser, @unchecked Sendable { private let coverageRef = Reference() private let heightRef = Reference() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary Capture(as: typeRef) { - try! Weather.Phenomenon.rx + Weather.Phenomenon.rx } transform: { .init(rawValue: String($0))! } " " Optionally { Capture(as: coverageRef) { - try! Remark.Coverage.rx + Remark.Coverage.rx } transform: { .init(rawValue: String($0)) } @@ -33,9 +32,8 @@ final class ObscurationParser: RemarkParser, @unchecked Sendable { Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let type = result[typeRef] let coverage = result[coverageRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservationTypeParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservationTypeParser.swift index ef7a352..426c030 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservationTypeParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservationTypeParser.swift @@ -24,7 +24,7 @@ final class ObservationTypeParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let typeStr = String(result[typeRef]).replacingOccurrences(of: "0", with: "O") let augmented = result[augmentedRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservedPrecipitationParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservedPrecipitationParser.swift index 5e595bb..b0bf0a4 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservedPrecipitationParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservedPrecipitationParser.swift @@ -8,19 +8,18 @@ final class ObservedPrecipitationParser: RemarkParser, @unchecked Sendable { private let proximityRef = Reference() private let directionsParser = RemarkDirectionsParser() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary Capture(as: precipRef) { - try! Remark.ObservedPrecipitationType.rx + Remark.ObservedPrecipitationType.rx } transform: { .init(rawValue: String($0))! } Optionally { " " Capture(as: proximityRef) { - try! Remark.Proximity.rx + Remark.Proximity.rx } transform: { .init(rawValue: String($0)) } @@ -32,9 +31,8 @@ final class ObservedPrecipitationParser: RemarkParser, @unchecked Sendable { Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let precip = result[precipRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservedVisibilityParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservedVisibilityParser.swift index 7a55906..4a995fd 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservedVisibilityParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/ObservedVisibilityParser.swift @@ -7,12 +7,11 @@ final class ObservedVisibilityParser: RemarkParser, @unchecked Sendable { private let sourceRef = Reference() private let visibilityParser = FractionParser() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary Capture(as: sourceRef) { - try! Remark.VisibilitySource.rx + Remark.VisibilitySource.rx } transform: { .init(rawValue: String($0))! } @@ -21,9 +20,8 @@ final class ObservedVisibilityParser: RemarkParser, @unchecked Sendable { Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let source = result[sourceRef] let distance = visibilityParser.parse(result) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/PeakWindsParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/PeakWindsParser.swift index 399cbef..f8251d2 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/PeakWindsParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/PeakWindsParser.swift @@ -30,7 +30,7 @@ final class PeakWindsParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let direction = result[directionRef] let speed = result[speedRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/PeriodicIceAccretionAmountParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/PeriodicIceAccretionAmountParser.swift index e718c11..e5e13a4 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/PeriodicIceAccretionAmountParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/PeriodicIceAccretionAmountParser.swift @@ -25,7 +25,7 @@ final class PeriodicIceAccretionAmountParser: RemarkParser, @unchecked Sendable } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let period = result[periodRef] let amount = result[amountRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/PeriodicPrecipitationAmountParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/PeriodicPrecipitationAmountParser.swift index 09576d8..f1df5ba 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/PeriodicPrecipitationAmountParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/PeriodicPrecipitationAmountParser.swift @@ -28,7 +28,7 @@ final class PeriodicPrecipitationAmountParser: RemarkParser, @unchecked Sendable } ) - func parse(remarks: inout String, date: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let period: UInt? = diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/PrecipitationBeginEndParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/PrecipitationBeginEndParser.swift index 0eef05c..b9faf8d 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/PrecipitationBeginEndParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/PrecipitationBeginEndParser.swift @@ -6,39 +6,35 @@ final class PrecipitationBeginEndParser: RemarkParser, @unchecked Sendable { private let typeRef = Reference() private let timeParser = HourMinuteParser() - // swiftlint:disable force_try private lazy var timeRx = LockedRegex( Regex { Capture(as: typeRef) { - try! Remark.EventType.rx + Remark.EventType.rx } transform: { .init(rawValue: String($0))! } timeParser.hourOptionalRx } ) - // swiftlint:enable force_try private let descriptorRef = Reference() private let phenomenonRef = Reference() private let timesRef = Reference() - // swiftlint:disable force_try private lazy var eventRx = LockedRegex( Regex { Capture(as: descriptorRef) { - try! Optionally(Weather.Descriptor.rx) + Optionally(Weather.Descriptor.rx) } transform: { .init(rawValue: String($0)) } Capture(as: phenomenonRef) { - try! Weather.Phenomenon.rx + Weather.Phenomenon.rx } transform: { .init(rawValue: String($0))! } Capture(as: timesRef) { OneOrMore(timeRx.composable) } } ) - // swiftlint:enable force_try private let eventsRef = Reference() private lazy var rx = LockedRegex( @@ -49,7 +45,7 @@ final class PrecipitationBeginEndParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let originalString = String(remarks[result.range]) @@ -67,7 +63,8 @@ final class PrecipitationBeginEndParser: RemarkParser, @unchecked Sendable { return .precipitationBeginEnd(events: events) } - private func parseEvents(from string: String, referenceDate: Date?, originalString: String) throws + private func parseEvents(from string: String, referenceDate: Date?, originalString: String) + throws(Error) -> [Remark.PrecipitationEvent] { let result = eventRx.allMatches(in: string) @@ -102,19 +99,20 @@ final class PrecipitationBeginEndParser: RemarkParser, @unchecked Sendable { return events } - private func parseTimes(from string: String, referenceDate: Date?, originalString: String) throws - -> [(Remark.EventType, DateComponents)] - { - let result = timeRx.allMatches(in: string) - guard !result.isEmpty else { return [] } - return try result.map { match in - let type = match[typeRef] + private func parseTimes( + from string: String, + referenceDate: Date?, + originalString: String + ) throws(Error) -> [(Remark.EventType, DateComponents)] { + var times = [(Remark.EventType, DateComponents)]() + for match in timeRx.allMatches(in: string) { let time = try timeParser.parse( match: match, referenceDate: referenceDate, originalString: originalString ) - return (type, time) + times.append((match[typeRef], time)) } + return times } } diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/PressureTendencyParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/PressureTendencyParser.swift index 91bcaa0..d039bf4 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/PressureTendencyParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/PressureTendencyParser.swift @@ -7,13 +7,12 @@ final class PressureTendencyParser: RemarkParser, @unchecked Sendable { private let characterRef = Reference() private let amountRef = Reference() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary "5" Capture(as: characterRef) { - try! Remark.PressureCharacter.rx + Remark.PressureCharacter.rx } transform: { .init(rawValue: String($0))! } @@ -25,9 +24,8 @@ final class PressureTendencyParser: RemarkParser, @unchecked Sendable { Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let character = result[characterRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/RapidPressureChangeParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/RapidPressureChangeParser.swift index 2a63c5d..650325e 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/RapidPressureChangeParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/RapidPressureChangeParser.swift @@ -5,21 +5,19 @@ final class RapidPressureChangeParser: RemarkParser, @unchecked Sendable { var urgency = Remark.Urgency.caution private let changeRef = Reference() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary Capture(as: changeRef) { - try! Remark.RapidPressureChange.rx + Remark.RapidPressureChange.rx } transform: { .init(rawValue: String($0))! } Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let change = result[changeRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/RapidSnowIncreaseParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/RapidSnowIncreaseParser.swift index 3e4eb84..08ad0b6 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/RapidSnowIncreaseParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/RapidSnowIncreaseParser.swift @@ -26,7 +26,7 @@ final class RapidSnowIncreaseParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let increase = result[increaseRef] let total = result[totalRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/RelativeHumidityParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/RelativeHumidityParser.swift index b1989d4..97a4c9e 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/RelativeHumidityParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/RelativeHumidityParser.swift @@ -18,7 +18,7 @@ final class RelativeHumidityParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let humidity = result[humidityRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/RunwayCeilingParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/RunwayCeilingParser.swift index 488893a..2d22f36 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/RunwayCeilingParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/RunwayCeilingParser.swift @@ -22,7 +22,7 @@ final class RunwayCeilingParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let height = result[heightRef] let runway = result[runwayRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/RunwayVisibilityParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/RunwayVisibilityParser.swift index 12d9936..6f1874c 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/RunwayVisibilityParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/RunwayVisibilityParser.swift @@ -18,7 +18,7 @@ final class RunwayVisibilityParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let distance = visibilityParser.parse(result) let runway = result[runwayRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/SeaLevelPressureParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/SeaLevelPressureParser.swift index 7f64e09..7b7f8b5 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/SeaLevelPressureParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/SeaLevelPressureParser.swift @@ -27,7 +27,7 @@ final class SeaLevelPressureParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } if let pressure = result[pressureRef] { diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/SectorVisibilityParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/SectorVisibilityParser.swift index 31ba77f..3fabf42 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/SectorVisibilityParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/SectorVisibilityParser.swift @@ -18,7 +18,7 @@ final class SectorVisibilityParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks), let direction = directionParser.parse(result) else { return nil } diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/SensorStatusParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/SensorStatusParser.swift index f2be2b9..62a10ec 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/SensorStatusParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/SensorStatusParser.swift @@ -8,19 +8,18 @@ final class SensorStatusParser: RemarkParser, @unchecked Sendable { private let secondarySensorRef = Reference() private let locationRef = Reference() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary ChoiceOf { Capture(as: sensorRef) { - try! Sensor.rx + Sensor.rx } transform: { .init(rawValue: String($0)) } Regex { Capture(as: secondarySensorRef) { - try! SecondarySensor.rx + SecondarySensor.rx } transform: { .init(rawValue: String($0)) } @@ -35,9 +34,8 @@ final class SensorStatusParser: RemarkParser, @unchecked Sendable { Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let sensor = result[sensorRef] let secondarySensor = result[secondarySensorRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/SignificantCloudsParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/SignificantCloudsParser.swift index 96962d3..1fa2d29 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/SignificantCloudsParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/SignificantCloudsParser.swift @@ -10,7 +10,6 @@ final class SignificantCloudsParser: RemarkParser, @unchecked Sendable { private let directionsParser = RemarkDirectionsParser() private let movingDirectionParser = RemarkDirectionParser() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary @@ -20,7 +19,7 @@ final class SignificantCloudsParser: RemarkParser, @unchecked Sendable { !$0.isEmpty } Capture(as: cloudTypeRef) { - try! Remark.SignificantCloudType.rx + Remark.SignificantCloudType.rx } transform: { .from(raw: String($0))! } @@ -40,9 +39,8 @@ final class SignificantCloudsParser: RemarkParser, @unchecked Sendable { Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let apparent = result[apparentRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/SixHourTemperatureExtremeParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/SixHourTemperatureExtremeParser.swift index 8fee01c..65ef212 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/SixHourTemperatureExtremeParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/SixHourTemperatureExtremeParser.swift @@ -7,12 +7,11 @@ final class SixHourTemperatureExtremeParser: RemarkParser, @unchecked Sendable { private let extremesRef = Reference() private let temperatureParser = NumericSignedIntegerParser(width: 3) - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary Capture(as: extremesRef) { - try! Remark.Extreme.rx + Remark.Extreme.rx } transform: { .init(rawValue: String($0))! } @@ -20,9 +19,8 @@ final class SixHourTemperatureExtremeParser: RemarkParser, @unchecked Sendable { Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks), let temperature = temperatureParser.parse(result) else { return nil } diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/SnowDepthParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/SnowDepthParser.swift index 26d0884..9853e07 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/SnowDepthParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/SnowDepthParser.swift @@ -18,7 +18,7 @@ final class SnowDepthParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let depth = result[depthRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/SunshineDurationParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/SunshineDurationParser.swift index a4754bf..ba81140 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/SunshineDurationParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/SunshineDurationParser.swift @@ -18,7 +18,7 @@ final class SunshineDurationParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let duration = result[durationRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/TemperatureDewpointParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/TemperatureDewpointParser.swift index 4ffde5f..c3e20ca 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/TemperatureDewpointParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/TemperatureDewpointParser.swift @@ -28,7 +28,7 @@ final class TemperatureDewpointParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks), let temperature = temperatureParser.parse(result) else { return nil } diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/ThunderstormBeginEndParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/ThunderstormBeginEndParser.swift index 0bd868a..5ddf952 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/ThunderstormBeginEndParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/ThunderstormBeginEndParser.swift @@ -6,18 +6,16 @@ final class ThunderstormBeginEndParser: RemarkParser, @unchecked Sendable { private let typeRef = Reference() private let timeParser = HourMinuteParser() - // swiftlint:disable force_try private lazy var eventRx = LockedRegex( Regex { Capture(as: typeRef) { - try! Remark.EventType.rx + Remark.EventType.rx } transform: { .init(rawValue: String($0))! } timeParser.hourOptionalRx } ) - // swiftlint:enable force_try private let eventsRef = Reference() private lazy var rx = LockedRegex( @@ -31,7 +29,7 @@ final class ThunderstormBeginEndParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let eventsStr = result[eventsRef] let referenceDate = zuluCal.date(from: date) @@ -41,18 +39,18 @@ final class ThunderstormBeginEndParser: RemarkParser, @unchecked Sendable { return .thunderstormBeginEnd(events: events) } - private func parseEvents(_ string: String, referenceDate: Date? = nil) throws -> [Remark + private func parseEvents(_ string: String, referenceDate: Date? = nil) throws(Error) -> [Remark .ThunderstormEvent] { - let result = eventRx.allMatches(in: string) - return try result.map { match in - let type = match[typeRef] + var events = [Remark.ThunderstormEvent]() + for match in eventRx.allMatches(in: string) { let time = try timeParser.parse( match: match, referenceDate: referenceDate, originalString: string ) - return .init(type: type, time: time) + events.append(.init(type: match[typeRef], time: time)) } + return events } } diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/ThunderstormLocationParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/ThunderstormLocationParser.swift index 217a7a8..a814fa1 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/ThunderstormLocationParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/ThunderstormLocationParser.swift @@ -8,7 +8,6 @@ final class ThunderstormLocationParser: RemarkParser, @unchecked Sendable { private let directionsParser = RemarkDirectionsParser() private let movingDirectionParser = RemarkDirectionParser() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary @@ -16,7 +15,7 @@ final class ThunderstormLocationParser: RemarkParser, @unchecked Sendable { Optionally { " " Capture(as: proximityRef) { - try! Remark.Proximity.rx + Remark.Proximity.rx } transform: { .init(rawValue: String($0)) } @@ -34,9 +33,8 @@ final class ThunderstormLocationParser: RemarkParser, @unchecked Sendable { Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let proximity = result[proximityRef] let directions = directionsParser.parse(result) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/TornadicActivityParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/TornadicActivityParser.swift index fd4d804..7d345f0 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/TornadicActivityParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/TornadicActivityParser.swift @@ -11,18 +11,17 @@ final class TornadicActivityParser: RemarkParser, @unchecked Sendable { private let directionParser = RemarkDirectionParser() private let movingDirectionParser = RemarkDirectionParser() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary Capture(as: typeRef) { - try! Remark.TornadicActivityType.rx + Remark.TornadicActivityType.rx } transform: { .init(rawValue: String($0))! } " " Capture(as: eventTypeRef) { - try! Remark.EventType.rx + Remark.EventType.rx } transform: { .init(rawValue: String($0))! } @@ -45,9 +44,8 @@ final class TornadicActivityParser: RemarkParser, @unchecked Sendable { Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks), let direction = directionParser.parse(result) else { return nil } diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableCeilingHeightParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableCeilingHeightParser.swift index 99ae6ca..60f6959 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableCeilingHeightParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableCeilingHeightParser.swift @@ -26,7 +26,7 @@ final class VariableCeilingHeightParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let low = result[lowRef] let high = result[highRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/VariablePrevailingVisibilityParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/VariablePrevailingVisibilityParser.swift index 0f762c7..b15e8a5 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/VariablePrevailingVisibilityParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/VariablePrevailingVisibilityParser.swift @@ -18,7 +18,7 @@ final class VariablePrevailingVisibilityParser: RemarkParser, @unchecked Sendabl } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let low = lowParser.parse(result) let high = highParser.parse(result) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableSkyConditionParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableSkyConditionParser.swift index 9887d67..308bf5f 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableSkyConditionParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableSkyConditionParser.swift @@ -8,12 +8,11 @@ final class VariableSkyConditionParser: RemarkParser, @unchecked Sendable { private let coverage2Ref = Reference() private let heightRef = Reference() - // swiftlint:disable force_try private lazy var rx = LockedRegex( Regex { Anchor.wordBoundary Capture(as: coverage1Ref) { - try! Remark.Coverage.rx + Remark.Coverage.rx } transform: { .init(rawValue: String($0))! } @@ -26,16 +25,15 @@ final class VariableSkyConditionParser: RemarkParser, @unchecked Sendable { } " V " Capture(as: coverage2Ref) { - try! Remark.Coverage.rx + Remark.Coverage.rx } transform: { .init(rawValue: String($0))! } Anchor.wordBoundary } ) - // swiftlint:enable force_try - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let coverage1 = result[coverage1Ref] let coverage2 = result[coverage2Ref] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableWindDirectionParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableWindDirectionParser.swift index 27c96c1..9ed810a 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableWindDirectionParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/VariableWindDirectionParser.swift @@ -26,7 +26,7 @@ final class VariableWindDirectionParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let dir1 = result[dir1Ref] let dir2 = result[dir2Ref] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/WaterEquivalentDepthParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/WaterEquivalentDepthParser.swift index 3a4875e..2fa6171 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/WaterEquivalentDepthParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/WaterEquivalentDepthParser.swift @@ -18,7 +18,7 @@ final class WaterEquivalentDepthParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let depth = result[depthRef] diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/WindChangeParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/WindChangeParser.swift index f9a0692..a5f197c 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/WindChangeParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/WindChangeParser.swift @@ -16,7 +16,7 @@ final class WindChangeParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let originalString = String(remarks[result.range]) let wind = try WindParser.parse(match: result, originalString: originalString) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/WindDataEstimatedParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/WindDataEstimatedParser.swift index efd7d03..2c54fc9 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/WindDataEstimatedParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/WindDataEstimatedParser.swift @@ -12,7 +12,7 @@ final class WindDataEstimatedParser: RemarkParser, @unchecked Sendable { var urgency = Remark.Urgency.routine - func parse(remarks: inout String, date _: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date _: DateComponents) throws(Error) -> Remark? { guard let result = try Self.rx.firstMatch(in: remarks) else { return nil } remarks.removeSubrange(result.range) diff --git a/Sources/SwiftMETAR/Remarks/Parsers/Types/WindShiftParser.swift b/Sources/SwiftMETAR/Remarks/Parsers/Types/WindShiftParser.swift index d8cba87..87e5ea1 100644 --- a/Sources/SwiftMETAR/Remarks/Parsers/Types/WindShiftParser.swift +++ b/Sources/SwiftMETAR/Remarks/Parsers/Types/WindShiftParser.swift @@ -23,7 +23,7 @@ final class WindShiftParser: RemarkParser, @unchecked Sendable { } ) - func parse(remarks: inout String, date: DateComponents) throws -> Remark? { + func parse(remarks: inout String, date: DateComponents) throws(Error) -> Remark? { guard let result = try rx.firstMatch(in: remarks) else { return nil } let originalString = String(remarks[result.range]) let referenceDate = zuluCal.date(from: date) diff --git a/Sources/SwiftMETAR/Remarks/Remark+Coded.swift b/Sources/SwiftMETAR/Remarks/Remark+Coded.swift index f6925e8..8257221 100644 --- a/Sources/SwiftMETAR/Remarks/Remark+Coded.swift +++ b/Sources/SwiftMETAR/Remarks/Remark+Coded.swift @@ -128,7 +128,7 @@ extension Remark: CodedRepresentable { - Parameter coded: The coded remark string. - Throws: An ``Error`` if `coded` contains no recognizable remark. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { var parts = coded.split(whereSeparator: \.isWhitespace) let date = zuluCal.dateComponents(in: zulu, from: Date()) let (entries, _) = try RemarksParser.parse( diff --git a/Sources/SwiftMETAR/Remarks/Remark+CodedR1.swift b/Sources/SwiftMETAR/Remarks/Remark+CodedR1.swift index ec59f40..f1471fe 100644 --- a/Sources/SwiftMETAR/Remarks/Remark+CodedR1.swift +++ b/Sources/SwiftMETAR/Remarks/Remark+CodedR1.swift @@ -5,12 +5,12 @@ extension Remark { static func coded(seaLevelPressure pressure: Float?) -> String { guard let pressure else { return "SLPNO" } let digits = Int(((pressure - 900) * 10).rounded()) - return "SLP\(String(format: "%03d", digits))" + return "SLP\(unsafe String(format: "%03d", digits))" } static func coded(pressureTendency character: PressureCharacter, change: Float) -> String { let magnitude = Int((abs(change) * 10).rounded()) - return "5\(character.rawValue)\(String(format: "%03d", magnitude))" + return "5\(character.rawValue)\(unsafe String(format: "%03d", magnitude))" } static func coded(rapidPressureChange change: RapidPressureChange) -> String { @@ -40,6 +40,6 @@ extension Remark { private static func codedTenths(_ value: Float) -> String { let sign = value < 0 ? "1" : "0" let magnitude = Int((abs(value) * 10).rounded()) - return "\(sign)\(String(format: "%03d", magnitude))" + return "\(sign)\(unsafe String(format: "%03d", magnitude))" } } diff --git a/Sources/SwiftMETAR/Remarks/Remark+CodedR2.swift b/Sources/SwiftMETAR/Remarks/Remark+CodedR2.swift index 73799cb..d203552 100644 --- a/Sources/SwiftMETAR/Remarks/Remark+CodedR2.swift +++ b/Sources/SwiftMETAR/Remarks/Remark+CodedR2.swift @@ -3,17 +3,17 @@ import Foundation extension Remark { static func coded(hourlyPrecipitationAmount amount: Float) -> String { - String(format: "P%04d", Int((amount * 100).rounded())) + unsafe String(format: "P%04d", Int((amount * 100).rounded())) } static func coded(dailyPrecipitationAmount amount: Float?) -> String { guard let amount else { return "7////" } - return String(format: "7%04d", Int((amount * 100).rounded())) + return unsafe String(format: "7%04d", Int((amount * 100).rounded())) } static func coded(periodicPrecipitationAmount _: UInt, amount: Float?) -> String { guard let amount else { return "6////" } - return String(format: "6%04d", Int((amount * 100).rounded())) + return unsafe String(format: "6%04d", Int((amount * 100).rounded())) } static func coded(precipitationBeginEnd events: [PrecipitationEvent]) -> String { @@ -25,18 +25,18 @@ extension Remark { result += group lastGroup = group } - let time = String(format: "%02d%02d", event.time.hour ?? 0, event.time.minute ?? 0) + let time = unsafe String(format: "%02d%02d", event.time.hour ?? 0, event.time.minute ?? 0) result += event.event.rawValue + time } return result } static func coded(waterEquivalentDepth depth: Float) -> String { - String(format: "933%03d", Int((depth * 10).rounded())) + unsafe String(format: "933%03d", Int((depth * 10).rounded())) } static func coded(snowDepth depth: UInt) -> String { - String(format: "4/%03d", Int(depth)) + unsafe String(format: "4/%03d", Int(depth)) } static func coded(rapidSnowIncrease depthIncrease: UInt, totalDepth: UInt) -> String { @@ -44,6 +44,6 @@ extension Remark { } static func coded(periodicIceAccretionAmount period: UInt8, amount: Float) -> String { - String(format: "I%d%03d", Int(period), Int((amount * 100).rounded())) + unsafe String(format: "I%d%03d", Int(period), Int((amount * 100).rounded())) } } diff --git a/Sources/SwiftMETAR/Remarks/Remark+CodedR3.swift b/Sources/SwiftMETAR/Remarks/Remark+CodedR3.swift index 1f0defe..303aa15 100644 --- a/Sources/SwiftMETAR/Remarks/Remark+CodedR3.swift +++ b/Sources/SwiftMETAR/Remarks/Remark+CodedR3.swift @@ -15,7 +15,7 @@ extension Remark { } static func coded(variableWindDirection heading1: UInt16, _ heading2: UInt16) -> String { - "WND \(String(format: "%03d", heading1))V\(String(format: "%03d", heading2))" + "WND \(unsafe String(format: "%03d", heading1))V\(unsafe String(format: "%03d", heading2))" } static func coded(windDataEstimated _: Void) -> String { @@ -33,14 +33,14 @@ extension Remark { /// Encodes an hour/minute time as the `HHMM` group used by wind remarks, /// falling back to a bare `MM` group when no hour is present. private static func hourMinute(_ time: DateComponents) -> String { - let minute = String(format: "%02d", time.minute ?? 0) + let minute = unsafe String(format: "%02d", time.minute ?? 0) guard let hour = time.hour else { return minute } - return "\(String(format: "%02d", hour))\(minute)" + return "\(unsafe String(format: "%02d", hour))\(minute)" } /// Encodes a day/hour time as the `ddhh` group used by TAF wind-change /// remarks, e.g. day 26 hour 14 → `"2614"`. private static func dayHour(_ time: DateComponents) -> String { - String(format: "%02d%02d", time.day ?? 0, time.hour ?? 0) + unsafe String(format: "%02d%02d", time.day ?? 0, time.hour ?? 0) } } diff --git a/Sources/SwiftMETAR/Remarks/Remark+CodedR4.swift b/Sources/SwiftMETAR/Remarks/Remark+CodedR4.swift index fd457ea..ad971dd 100644 --- a/Sources/SwiftMETAR/Remarks/Remark+CodedR4.swift +++ b/Sources/SwiftMETAR/Remarks/Remark+CodedR4.swift @@ -17,7 +17,7 @@ extension Remark { /// Codes a ceiling/cloud height (in feet) as the parser's three-digit /// hundreds-of-feet token, e.g. `500` → `"005"`. static func codedHundredsOfFeet(_ height: UInt) -> String { - String(format: "%03d", Int(height / 100)) + unsafe String(format: "%03d", Int(height / 100)) } static func coded(observedVisibility source: VisibilitySource, distance: Ratio) -> String { diff --git a/Sources/SwiftMETAR/Remarks/Remark+CodedR5.swift b/Sources/SwiftMETAR/Remarks/Remark+CodedR5.swift index 12331a4..507b7f0 100644 --- a/Sources/SwiftMETAR/Remarks/Remark+CodedR5.swift +++ b/Sources/SwiftMETAR/Remarks/Remark+CodedR5.swift @@ -87,9 +87,9 @@ extension Remark { /// Formats an event time as `HHMM` (or `MM` when the hour is unknown). private static func codedHourMinute(_ components: DateComponents) -> String { if let hour = components.hour { - return String(format: "%02d%02d", hour, components.minute ?? 0) + return unsafe String(format: "%02d%02d", hour, components.minute ?? 0) } - return String(format: "%02d", components.minute ?? 0) + return unsafe String(format: "%02d", components.minute ?? 0) } /// Concatenates lightning types in canonical order (`CG`, `IC`, `CC`, `CA`). diff --git a/Sources/SwiftMETAR/Remarks/Remark+CodedR6.swift b/Sources/SwiftMETAR/Remarks/Remark+CodedR6.swift index ea22f43..1201564 100644 --- a/Sources/SwiftMETAR/Remarks/Remark+CodedR6.swift +++ b/Sources/SwiftMETAR/Remarks/Remark+CodedR6.swift @@ -25,7 +25,7 @@ extension Remark { } static func coded(sunshineDuration duration: UInt) -> String { - "98\(String(format: "%03d", duration))" + "98\(unsafe String(format: "%03d", duration))" } static func coded(navalForecasterCenter center: NavalWeatherCenter, ID: UInt) -> String { diff --git a/Sources/SwiftMETAR/Remarks/Remark+CodedR7.swift b/Sources/SwiftMETAR/Remarks/Remark+CodedR7.swift index 377556b..7fd7d2d 100644 --- a/Sources/SwiftMETAR/Remarks/Remark+CodedR7.swift +++ b/Sources/SwiftMETAR/Remarks/Remark+CodedR7.swift @@ -5,7 +5,7 @@ extension Remark { /// The coded form of a ``correction(time:)`` remark, e.g. `"COR 1547"` /// (the correction hour and minute, `HHMM`). static func coded(correctionTime time: DateComponents) -> String { - "COR " + String(format: "%02d%02d", time.hour ?? 0, time.minute ?? 0) + "COR " + (unsafe String(format: "%02d%02d", time.hour ?? 0, time.minute ?? 0)) } /// The coded form of a ``next(_:)`` remark, e.g. `"NEXT 1518"` (the next @@ -21,6 +21,6 @@ extension Remark { } private static func codedDayHour(_ date: DateComponents) -> String { - String(format: "%02d%02d", date.day ?? 0, date.hour ?? 0) + unsafe String(format: "%02d%02d", date.day ?? 0, date.hour ?? 0) } } diff --git a/Sources/SwiftMETAR/Support/LockedRegex.swift b/Sources/SwiftMETAR/Support/LockedRegex.swift index f0e41df..a0d3581 100644 --- a/Sources/SwiftMETAR/Support/LockedRegex.swift +++ b/Sources/SwiftMETAR/Support/LockedRegex.swift @@ -41,23 +41,23 @@ final class LockedRegex: @unchecked Sendable { self.build = build } - func wholeMatch(in string: Substring) throws -> Regex.Match? { - try regex.wholeMatch(in: string) + func wholeMatch(in string: Substring) throws(Error) -> Regex.Match? { + try narrowingErrors { try regex.wholeMatch(in: string) } } - func wholeMatch(in string: String) throws -> Regex.Match? { + func wholeMatch(in string: String) throws(Error) -> Regex.Match? { try wholeMatch(in: string[...]) } - func firstMatch(in string: Substring) throws -> Regex.Match? { - try regex.firstMatch(in: string) + func firstMatch(in string: Substring) throws(Error) -> Regex.Match? { + try narrowingErrors { try regex.firstMatch(in: string) } } - func firstMatch(in string: String) throws -> Regex.Match? { + func firstMatch(in string: String) throws(Error) -> Regex.Match? { try firstMatch(in: string[...]) } - func matches(_ string: some StringProtocol) throws -> Bool { + func matches(_ string: some StringProtocol) throws(Error) -> Bool { try wholeMatch(in: Substring(string)) != nil } @@ -65,6 +65,23 @@ final class LockedRegex: @unchecked Sendable { Array(string.matches(of: regex)) } + /// Narrows the untyped error `Regex` matching declares to this package's ``Error``. + /// + /// Matching only throws what a `transform:` closure in the regex throws, and every + /// transform in this package throws an ``Error``. Anything else would mean the regex + /// program itself failed to run, which callers can only treat as an unparseable product. + private func narrowingErrors( + _ match: () throws -> Regex.Match? + ) throws(Error) -> Regex.Match? { + do { + return try match() + } catch let error as Error { + throw error + } catch { + throw .badFormat + } + } + private func constructAndCache() -> Regex { cache.withLock { slot in if let cached = slot { return cached } diff --git a/Sources/SwiftMETAR/Support/RegexAlternation.swift b/Sources/SwiftMETAR/Support/RegexAlternation.swift new file mode 100644 index 0000000..cb01d47 --- /dev/null +++ b/Sources/SwiftMETAR/Support/RegexAlternation.swift @@ -0,0 +1,20 @@ +import RegexBuilder + +/** + Builds a regex matching any one of `alternatives`, tried in the order given. + + `ChoiceOf` only accepts a statically-known list of alternatives, so a run-time list is + folded with the same builder `ChoiceOf` uses. Each alternative is matched literally, + which keeps regex metacharacters in the alternatives from being interpreted. + + - Parameter alternatives: The strings to match, in the order the engine should try them. + - Returns: A regex matching the first alternative that matches. + */ +func regexAlternation(of alternatives: [String]) -> Regex { + guard let firstAlternative = alternatives.first else { + preconditionFailure("A regex alternation needs at least one alternative") + } + return alternatives.dropFirst().reduce(Regex { firstAlternative }) { alternation, alternative in + AlternationBuilder.buildPartialBlock(accumulated: alternation, next: alternative).regex + } +} diff --git a/Sources/SwiftMETAR/Support/RegexCases.swift b/Sources/SwiftMETAR/Support/RegexCases.swift index 66a94c7..f4f8a5d 100644 --- a/Sources/SwiftMETAR/Support/RegexCases.swift +++ b/Sources/SwiftMETAR/Support/RegexCases.swift @@ -1,17 +1,29 @@ -import Foundation import RegexBuilder +import Synchronization +/// A type whose cases are recognized in coded weather products by their raw values. protocol RegexCases: RawRepresentable, CaseIterable { - static var rx: Regex { get throws } + + /// A regex matching any one of this type's raw values, tried in declaration order. + static var rx: Regex { get } } +/// Holds one compiled alternation per conforming type, so each case set is built once per +/// process instead of on every access. +/// +/// Like ``LockedRegex/composable``, this builds without taking the process-wide regex +/// construction lock: a case alternation is only ever reached while a parent `Regex { }` is +/// being built, so that non-reentrant lock is already held. +private let caseAlternations = Mutex<[ObjectIdentifier: Regex]>([:]) + extension RegexCases where RawValue == String { static var rx: Regex { - get throws { - let cases = allCases.map { NSRegularExpression.escapedPattern(for: $0.rawValue) }.joined( - separator: "|" - ) - return try Regex(cases) + caseAlternations.withLock { cache in + let key = ObjectIdentifier(Self.self) + if let cached = cache[key] { return cached } + let alternation = regexAlternation(of: allCases.map(\.rawValue)) + cache[key] = alternation + return alternation } } } diff --git a/Sources/SwiftMETAR/Support/XMLParsing.swift b/Sources/SwiftMETAR/Support/XMLParsing.swift index f2d675e..3b67a5a 100644 --- a/Sources/SwiftMETAR/Support/XMLParsing.swift +++ b/Sources/SwiftMETAR/Support/XMLParsing.swift @@ -5,21 +5,22 @@ enum XMLParsing { // MARK: - Type Methods - static func parseISO8601(_ string: String) throws -> DateComponents { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - if let date = formatter.date(from: string) { - return zuluCal.dateComponents(in: zulu, from: date) - } - // Fall back to parsing without fractional seconds - formatter.formatOptions = [.withInternetDateTime] - guard let date = formatter.date(from: string) else { - throw Error.invalidDate(string) - } + private static let iso8601WithFractionalSeconds = Date.ISO8601FormatStyle( + includingFractionalSeconds: true + ) + private static let iso8601 = Date.ISO8601FormatStyle() + + static func parseISO8601(_ string: String) throws(Error) -> DateComponents { + guard + let date = + (try? iso8601WithFractionalSeconds.parse(string)) ?? (try? iso8601.parse(string)) + else { throw Error.invalidDate(string) } return zuluCal.dateComponents(in: zulu, from: date) } - static func buildWind(dirDegrees: String?, speedKt: String?, gustKt: String?) throws -> Wind? { + static func buildWind(dirDegrees: String?, speedKt: String?, gustKt: String?) throws(Error) + -> Wind? + { guard let speedStr = speedKt else { return nil } guard let speed = UInt16(speedStr) else { throw Error.invalidWinds(speedStr) @@ -49,7 +50,7 @@ enum XMLParsing { return nil } - static func buildVisibility(_ visStr: String?) throws -> Visibility? { + static func buildVisibility(_ visStr: String?) throws(Error) -> Visibility? { guard let visStr, !visStr.isEmpty else { return nil } // Handle "10+" or "6+" format (greater than) @@ -80,7 +81,7 @@ enum XMLParsing { return .equal(.statuteMilesDecimal(value)) } - static func buildWeather(_ wxString: String?) throws -> [Weather]? { + static func buildWeather(_ wxString: String?) throws(Error) -> [Weather]? { guard let wxString, !wxString.isEmpty else { return [] } let tokens = wxString.split(separator: " ") @@ -92,7 +93,7 @@ enum XMLParsing { static func buildConditions( skyConditions: [SkyCondition], vertVisFt: String? - ) throws -> [Condition] { + ) throws(Error) -> [Condition] { var conditions = [Condition]() for sky in skyConditions { @@ -139,7 +140,7 @@ enum XMLParsing { return conditions } - static func buildAltimeter(_ altimStr: String?) throws -> Altimeter? { + static func buildAltimeter(_ altimStr: String?) throws(Error) -> Altimeter? { guard let altimStr else { return nil } guard let value = Float(altimStr) else { throw Error.invalidAltimeter(altimStr) diff --git a/Sources/SwiftMETAR/TAF/Icing.swift b/Sources/SwiftMETAR/TAF/Icing.swift index 8ff8275..9226be2 100644 --- a/Sources/SwiftMETAR/TAF/Icing.swift +++ b/Sources/SwiftMETAR/TAF/Icing.swift @@ -78,7 +78,7 @@ extension Icing: CodedRepresentable { feet are truncated by integer division and do not round-trip. */ public var codedString: String { - "6\(type.rawValue)\(String(format: "%03d", base / 100))\(depth / 1000)" + "6\(type.rawValue)\(unsafe String(format: "%03d", base / 100))\(depth / 1000)" } /** @@ -88,7 +88,7 @@ extension Icing: CodedRepresentable { - Throws: ``Error/invalidIcing(_:)`` if `coded` is not a valid icing representation. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { var parts = coded.split(whereSeparator: \.isWhitespace) guard let icing = try IcingParser().parse(&parts), parts.isEmpty else { throw Error.invalidIcing(coded) diff --git a/Sources/SwiftMETAR/TAF/Parsers/IcingParser.swift b/Sources/SwiftMETAR/TAF/Parsers/IcingParser.swift index 2a50de5..ed2ddd0 100644 --- a/Sources/SwiftMETAR/TAF/Parsers/IcingParser.swift +++ b/Sources/SwiftMETAR/TAF/Parsers/IcingParser.swift @@ -29,7 +29,7 @@ final class IcingParser: WarmableParser, @unchecked Sendable { } ) - func parse(_ parts: inout [String.SubSequence]) throws -> Icing? { + func parse(_ parts: inout [String.SubSequence]) throws(Error) -> Icing? { guard !parts.isEmpty else { return nil } let icingStr = String(parts[0]) guard let result = try Self.rx.wholeMatch(in: icingStr) else { return nil } diff --git a/Sources/SwiftMETAR/TAF/Parsers/PeriodParser.swift b/Sources/SwiftMETAR/TAF/Parsers/PeriodParser.swift index 996f287..8c9126f 100644 --- a/Sources/SwiftMETAR/TAF/Parsers/PeriodParser.swift +++ b/Sources/SwiftMETAR/TAF/Parsers/PeriodParser.swift @@ -18,7 +18,8 @@ final class PeriodParser: WarmableParser, @unchecked Sendable { for subparser in subparsers { subparser.warmUp() } } - func parse(_ parts: inout [String.SubSequence], referenceDate: Date? = nil) throws -> TAF.Group + func parse(_ parts: inout [String.SubSequence], referenceDate: Date? = nil) throws(Error) -> TAF + .Group .Period? { for subparser in subparsers { @@ -32,7 +33,8 @@ final class PeriodParser: WarmableParser, @unchecked Sendable { private protocol Subparser: AnyObject { func warmUp() - func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws -> TAF.Group.Period? + func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws(Error) -> TAF.Group + .Period? } private final class FromPeriodParser: Subparser { @@ -50,7 +52,8 @@ final class PeriodParser: WarmableParser, @unchecked Sendable { _ = try? rx.wholeMatch(in: "") } - func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws -> TAF.Group.Period? + func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws(Error) -> TAF.Group + .Period? { guard !parts.isEmpty else { return nil } @@ -85,7 +88,8 @@ final class PeriodParser: WarmableParser, @unchecked Sendable { _ = try? rx.wholeMatch(in: "") } - func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws -> TAF.Group.Period? + func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws(Error) -> TAF.Group + .Period? { guard let period = try parseRange(&parts, referenceDate: referenceDate) else { return nil @@ -93,7 +97,7 @@ final class PeriodParser: WarmableParser, @unchecked Sendable { return .range(period) } - func parseRange(_ parts: inout [String.SubSequence], referenceDate: Date) throws + func parseRange(_ parts: inout [String.SubSequence], referenceDate: Date) throws(Error) -> DateComponentsInterval? { guard !parts.isEmpty else { return nil } @@ -133,7 +137,8 @@ final class PeriodParser: WarmableParser, @unchecked Sendable { func warmUp() {} - func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws -> TAF.Group.Period? + func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws(Error) -> TAF.Group + .Period? { guard !parts.isEmpty else { return nil } guard parts[0] == "TEMPO" else { return nil } @@ -156,7 +161,8 @@ final class PeriodParser: WarmableParser, @unchecked Sendable { func warmUp() {} - func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws -> TAF.Group.Period? + func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws(Error) -> TAF.Group + .Period? { guard !parts.isEmpty else { return nil } guard parts[0] == "BECMG" else { return nil } @@ -194,7 +200,8 @@ final class PeriodParser: WarmableParser, @unchecked Sendable { _ = try? rx.wholeMatch(in: "") } - func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws -> TAF.Group.Period? + func parse(_ parts: inout [String.SubSequence], referenceDate: Date) throws(Error) -> TAF.Group + .Period? { guard !parts.isEmpty else { return nil } diff --git a/Sources/SwiftMETAR/TAF/Parsers/TAFParser.swift b/Sources/SwiftMETAR/TAF/Parsers/TAFParser.swift index 0b8ea19..368a5de 100644 --- a/Sources/SwiftMETAR/TAF/Parsers/TAFParser.swift +++ b/Sources/SwiftMETAR/TAF/Parsers/TAFParser.swift @@ -18,15 +18,25 @@ actor TAFParser { /// Parses a TAF synchronously. Used by the synchronous `Codable` decode path; shares /// the same cached parsers as the async path. - static func parseSynchronously(_ codedTAF: String, on referenceDate: Date? = nil) throws -> TAF { - try assemble(codedTAF, referenceDate: referenceDate) { parts, date, lenient in - try RemarksParser.parse( - &parts, - using: RemarksParser.sharedParsers, - date: date, - lenientRemarks: lenient - ) - } + static func parseSynchronously( + _ codedTAF: String, + on referenceDate: Date? = nil + ) throws(Error) -> TAF { + try assemble(codedTAF, referenceDate: referenceDate, parseRemarks: parseSharedRemarks) + } + + /// Parses remarks with the shared parser set, for callers that need no lenient-mode override. + private static func parseSharedRemarks( + _ parts: inout [Substring], + _ date: DateComponents, + _ lenient: Bool + ) throws(Error) -> ([RemarkEntry], String?) { + try RemarksParser.parse( + &parts, + using: RemarksParser.sharedParsers, + date: date, + lenientRemarks: lenient + ) } /// The isolation-free parsing core, shared by the async and synchronous paths. @@ -34,10 +44,11 @@ actor TAFParser { private static func assemble( _ codedTAF: String, referenceDate: Date?, - parseRemarks: (_ parts: inout [Substring], _ date: DateComponents, _ lenient: Bool) throws -> ( - [RemarkEntry], String? - ) - ) throws -> TAF { + parseRemarks: (_ parts: inout [Substring], _ date: DateComponents, _ lenient: Bool) + throws(Error) -> ( + [RemarkEntry], String? + ) + ) throws(Error) -> TAF { var parts = codedTAF.split(separator: .whitespacesAndNewlines) let issuance = try parseIssuance(&parts) @@ -153,7 +164,7 @@ actor TAFParser { /// Parses a single forecast group from its coded string (e.g. /// `"FM130200 05005KT P6SM SCT040"`), synchronously. Used by /// `TAF.Group.init(coded:)`. - static func parseGroup(_ coded: String) throws -> TAF.Group { + static func parseGroup(_ coded: String) throws(Error) -> TAF.Group { var parts = coded.split(separator: .whitespacesAndNewlines) guard let period = try periodParser.parse(&parts, referenceDate: nil) else { throw Error.invalidPeriod(coded) @@ -202,7 +213,9 @@ actor TAFParser { ) } - private static func parseIssuance(_ parts: inout [String.SubSequence]) throws -> TAF.Issuance { + private static func parseIssuance(_ parts: inout [String.SubSequence]) throws(Error) + -> TAF.Issuance + { guard !parts.isEmpty else { throw Error.badFormat } if parts[0] != "TAF" { return .routine } @@ -221,14 +234,11 @@ actor TAFParser { } } - func parse(_ codedTAF: String, on referenceDate: Date? = nil) throws -> TAF { - try Self.assemble(codedTAF, referenceDate: referenceDate) { parts, date, lenient in - try RemarksParser.parse( - &parts, - using: RemarksParser.sharedParsers, - date: date, - lenientRemarks: lenient - ) - } + func parse(_ codedTAF: String, on referenceDate: Date? = nil) throws(Error) -> TAF { + try Self.assemble( + codedTAF, + referenceDate: referenceDate, + parseRemarks: Self.parseSharedRemarks + ) } } diff --git a/Sources/SwiftMETAR/TAF/Parsers/TAFTemperatureParser.swift b/Sources/SwiftMETAR/TAF/Parsers/TAFTemperatureParser.swift index 781a523..ec00645 100644 --- a/Sources/SwiftMETAR/TAF/Parsers/TAFTemperatureParser.swift +++ b/Sources/SwiftMETAR/TAF/Parsers/TAFTemperatureParser.swift @@ -52,7 +52,8 @@ final class TAFTemperatureParser: WarmableParser, @unchecked Sendable { _ = try? rx.wholeMatch(in: "") } - func parse(_ parts: inout [String.SubSequence], date: DateComponents) throws -> [TAF.Temperature]? + func parse(_ parts: inout [String.SubSequence], date: DateComponents) throws(Error) -> [TAF + .Temperature]? { var temps = [TAF.Temperature]() diff --git a/Sources/SwiftMETAR/TAF/Parsers/TAFXMLParser.swift b/Sources/SwiftMETAR/TAF/Parsers/TAFXMLParser.swift index cc7dd0b..6c06c86 100644 --- a/Sources/SwiftMETAR/TAF/Parsers/TAFXMLParser.swift +++ b/Sources/SwiftMETAR/TAF/Parsers/TAFXMLParser.swift @@ -13,7 +13,7 @@ actor TAFXMLParser { // MARK: - Type Methods - private static func buildTAF(from entry: Entry) throws -> TAF { + private static func buildTAF(from entry: Entry) throws(Error) -> TAF { guard let stationID = entry.stationID else { throw Error.badFormat } @@ -47,7 +47,9 @@ actor TAFXMLParser { ) } - private static func buildGroup(from forecast: ForecastEntry, isFirst: Bool) throws -> TAF.Group { + private static func buildGroup(from forecast: ForecastEntry, isFirst: Bool) throws(Error) + -> TAF.Group + { let period = try buildPeriod(from: forecast, isFirst: isFirst) let wind = try XMLParsing.buildWind( @@ -65,8 +67,8 @@ actor TAFXMLParser { let windshear = buildWindshear(from: forecast) - let turbulence = try forecast.turbulenceConditions.map { try buildTurbulence(from: $0) } - let icing = try forecast.icingConditions.map { try buildIcing(from: $0) } + let turbulence = try forecast.turbulenceConditions.map(buildTurbulence(from:)) + let icing = try forecast.icingConditions.map(buildIcing(from:)) let altimeter = try XMLParsing.buildAltimeter(forecast.altimInHg) @@ -87,7 +89,7 @@ actor TAFXMLParser { ) } - private static func buildPeriod(from forecast: ForecastEntry, isFirst _: Bool) throws + private static func buildPeriod(from forecast: ForecastEntry, isFirst _: Bool) throws(Error) -> TAF .Group .Period @@ -136,7 +138,7 @@ actor TAFXMLParser { ) } - private static func buildTurbulence(from entry: TurbulenceEntry) throws -> Turbulence { + private static func buildTurbulence(from entry: TurbulenceEntry) throws(Error) -> Turbulence { guard let intensityStr = entry.intensity else { throw Error.invalidTurbulence("") } @@ -203,7 +205,7 @@ actor TAFXMLParser { ) } - private static func buildIcing(from entry: IcingEntry) throws -> Icing { + private static func buildIcing(from entry: IcingEntry) throws(Error) -> Icing { guard let intensityStr = entry.intensity else { throw Error.invalidIcing("") } @@ -225,7 +227,9 @@ actor TAFXMLParser { func parse(data: Data) -> [XMLParseResult] { let delegate = XMLDelegate() let xmlParser = XMLParser(data: data) - xmlParser.delegate = delegate + // `XMLParser.delegate` is `unowned(unsafe)`; `delegate` is held strongly by this + // frame for the whole parse and is read again afterwards, so it outlives the parser. + unsafe xmlParser.delegate = delegate xmlParser.parse() return delegate.entries.map { entry in diff --git a/Sources/SwiftMETAR/TAF/Parsers/TurbulenceParser.swift b/Sources/SwiftMETAR/TAF/Parsers/TurbulenceParser.swift index 5f09305..7d795f4 100644 --- a/Sources/SwiftMETAR/TAF/Parsers/TurbulenceParser.swift +++ b/Sources/SwiftMETAR/TAF/Parsers/TurbulenceParser.swift @@ -29,7 +29,7 @@ final class TurbulenceParser: WarmableParser, @unchecked Sendable { } ) - func parse(_ parts: inout [String.SubSequence]) throws -> Turbulence? { + func parse(_ parts: inout [String.SubSequence]) throws(Error) -> Turbulence? { guard !parts.isEmpty else { return nil } let turbStr = String(parts[0]) guard let result = try Self.rx.wholeMatch(in: turbStr) else { return nil } diff --git a/Sources/SwiftMETAR/TAF/Parsers/WindshearConditionsParser.swift b/Sources/SwiftMETAR/TAF/Parsers/WindshearConditionsParser.swift index ee9b6b7..ed12761 100644 --- a/Sources/SwiftMETAR/TAF/Parsers/WindshearConditionsParser.swift +++ b/Sources/SwiftMETAR/TAF/Parsers/WindshearConditionsParser.swift @@ -1,6 +1,6 @@ import Foundation -func parseWindshearConditions(_ parts: inout [String.SubSequence]) throws -> Bool { +func parseWindshearConditions(_ parts: inout [String.SubSequence]) throws(Error) -> Bool { guard !parts.isEmpty else { return false } if parts[0] == "WSCONDS" { diff --git a/Sources/SwiftMETAR/TAF/TAF+Coded.swift b/Sources/SwiftMETAR/TAF/TAF+Coded.swift index e610545..4611195 100644 --- a/Sources/SwiftMETAR/TAF/TAF+Coded.swift +++ b/Sources/SwiftMETAR/TAF/TAF+Coded.swift @@ -40,7 +40,7 @@ extension TAF: CodedRepresentable { - Parameter coded: The coded TAF string. - Throws: An ``Error`` if `coded` is not a valid TAF. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { self = try TAFParser.parseSynchronously(coded) } } @@ -77,7 +77,7 @@ extension TAF.Group: CodedRepresentable { - Parameter coded: The coded group string. - Throws: An ``Error`` if `coded` is not a valid forecast group. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { self = try TAFParser.parseGroup(coded) } @@ -85,8 +85,8 @@ extension TAF.Group: CodedRepresentable { /// form rather than the METAR `A####`/`Q####` form. private static func tafAltimeter(_ altimeter: Altimeter) -> String { switch altimeter { - case .inHg(let value): "QNH\(String(format: "%04d", value))INS" - case .hPa(let value): "QNH\(String(format: "%04d", value))HPA" + case .inHg(let value): "QNH\(unsafe String(format: "%04d", value))INS" + case .hPa(let value): "QNH\(unsafe String(format: "%04d", value))HPA" } } } @@ -97,9 +97,9 @@ extension TAF.Temperature { /// `"TX17/2419Z"` (maximum) or `"TNM04/2509Z"` (minimum). public var codedString: String { let letter = type == .minimum ? "N" : "X" - let magnitude = String(format: "%02d", abs(value)) + let magnitude = unsafe String(format: "%02d", abs(value)) let coded = value < 0 ? "M\(magnitude)" : magnitude - let dayHour = String(format: "%02d%02d", time.day ?? 0, time.hour ?? 0) + let dayHour = unsafe String(format: "%02d%02d", time.day ?? 0, time.hour ?? 0) return "T\(letter)\(coded)/\(dayHour)Z" } } diff --git a/Sources/SwiftMETAR/TAF/TAF.swift b/Sources/SwiftMETAR/TAF/TAF.swift index 9b7b4a6..4320410 100644 --- a/Sources/SwiftMETAR/TAF/TAF.swift +++ b/Sources/SwiftMETAR/TAF/TAF.swift @@ -55,7 +55,7 @@ public struct TAF: Sendable { - Returns: The parsed TAF. - Throws: If a parsing error occurs. */ - public static func from(string: String, on date: Date? = nil) async throws -> Self { + public static func from(string: String, on date: Date? = nil) async throws(Error) -> Self { return try await TAFParser.shared.parse(string, on: date) } @@ -303,7 +303,7 @@ public struct TAF: Sendable { case .temporary(let period): "TEMPO \(Self.coded(period))" case .becoming(let period): "BECMG \(Self.coded(period))" case let .probability(probability, period): - "PROB\(String(format: "%02d", probability)) \(Self.coded(period))" + "PROB\(unsafe String(format: "%02d", probability)) \(Self.coded(period))" } } @@ -317,7 +317,7 @@ public struct TAF: Sendable { - Parameter coded: The coded string. - Throws: ``Error/invalidPeriod(_:)`` if `coded` is not a valid period. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { var parts = coded.split(whereSeparator: \.isWhitespace) guard let period = try PeriodParser().parse(&parts), parts.isEmpty else { throw Error.invalidPeriod(coded) @@ -330,11 +330,11 @@ public struct TAF: Sendable { } private static func dayHour(_ components: DateComponents) -> String { - String(format: "%02d%02d", components.day ?? 0, components.hour ?? 0) + unsafe String(format: "%02d%02d", components.day ?? 0, components.hour ?? 0) } private static func dayHourMinute(_ components: DateComponents) -> String { - String( + unsafe String( format: "%02d%02d%02d", components.day ?? 0, components.hour ?? 0, diff --git a/Sources/SwiftMETAR/TAF/Turbulence.swift b/Sources/SwiftMETAR/TAF/Turbulence.swift index 1ab621f..97ca9a0 100644 --- a/Sources/SwiftMETAR/TAF/Turbulence.swift +++ b/Sources/SwiftMETAR/TAF/Turbulence.swift @@ -88,7 +88,7 @@ extension Turbulence: CodedRepresentable { three digits) and the ``depth`` in thousands of feet (a single digit). */ public var codedString: String { - "5\(typeFigure)\(String(format: "%03d", base / 100))\(depth / 1000)" + "5\(typeFigure)\(unsafe String(format: "%03d", base / 100))\(depth / 1000)" } private var typeFigure: Character { @@ -114,7 +114,7 @@ extension Turbulence: CodedRepresentable { - Throws: ``Error/invalidTurbulence(_:)`` if `coded` is not a valid turbulence representation. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { var parts = coded.split(whereSeparator: \.isWhitespace) guard let turbulence = try TurbulenceParser().parse(&parts), parts.isEmpty else { throw Error.invalidTurbulence(coded) diff --git a/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftDataGroupParser.swift b/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftDataGroupParser.swift index 22b75b5..266b75c 100644 --- a/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftDataGroupParser.swift +++ b/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftDataGroupParser.swift @@ -109,7 +109,7 @@ final class WindsAloftDataGroupParser: WarmableParser, @unchecked Sendable { /// speed = ff+100 /// - `9900`: light and variable, carrying the temperature of whichever form /// it appears in (`9900`, `9900±TT`, or `9900TT`) - func parse(_ group: String) throws -> WindsAloftEntry? { + func parse(_ group: String) throws(Error) -> WindsAloftEntry? { let trimmed = group.trimmingCharacters(in: .whitespaces) guard !trimmed.isEmpty else { return nil } guard let components = try components(of: trimmed) else { @@ -131,7 +131,7 @@ final class WindsAloftDataGroupParser: WarmableParser, @unchecked Sendable { /// Matches `group` against each supported data group format, or returns `nil` /// if it matches none of them. - private func components(of group: String) throws -> Components? { + private func components(of group: String) throws(Error) -> Components? { if let match = try Self.signedRx.wholeMatch(in: group) { return .init(match: match, temperature: match[Self.signedTempRef]) } diff --git a/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftHeaderParser.swift b/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftHeaderParser.swift index 1b29213..9afe2da 100644 --- a/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftHeaderParser.swift +++ b/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftHeaderParser.swift @@ -69,7 +69,7 @@ final class WindsAloftHeaderParser: WarmableParser, @unchecked Sendable { func parse( _ lines: inout [String], referenceDate: Date? - ) throws -> ( + ) throws(Error) -> ( header: WindsAloft.Header, basedOn: DateComponents, validAt: DateComponents, diff --git a/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftParser.swift b/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftParser.swift index ee62830..fb5d8d1 100644 --- a/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftParser.swift +++ b/Sources/SwiftMETAR/WindsAloft/Parsers/WindsAloftParser.swift @@ -13,7 +13,7 @@ actor WindsAloftParser { nonisolated static func parseSynchronously( _ string: String, on referenceDate: Date? = nil - ) throws -> WindsAloft { + ) throws(Error) -> WindsAloft { try assemble(string, referenceDate: referenceDate) } @@ -22,7 +22,7 @@ actor WindsAloftParser { nonisolated private static func assemble( _ string: String, referenceDate: Date? - ) throws -> WindsAloft { + ) throws(Error) -> WindsAloft { var lines = string.components(separatedBy: .newlines) let (header, basedOn, validAt, usePeriod) = @@ -70,7 +70,7 @@ actor WindsAloftParser { // MARK: - Column Layout - nonisolated private static func parseColumnLayout(_ line: String) throws -> ColumnLayout { + nonisolated private static func parseColumnLayout(_ line: String) throws(Error) -> ColumnLayout { guard line.contains("FT") || line.contains("3000") || line.contains("6000") else { throw Error.invalidWindsAloftColumns(line) } @@ -129,7 +129,7 @@ actor WindsAloftParser { nonisolated private static func parseStationLine( _ line: String, layout: ColumnLayout - ) throws -> WindsAloft.Station { + ) throws(Error) -> WindsAloft.Station { let lineLength = line.count // Extract station ID from the station range @@ -161,7 +161,7 @@ actor WindsAloftParser { return WindsAloft.Station(id: stationID, entries: entries) } - func parse(_ string: String, on referenceDate: Date? = nil) throws -> WindsAloft { + func parse(_ string: String, on referenceDate: Date? = nil) throws(Error) -> WindsAloft { try Self.assemble(string, referenceDate: referenceDate) } diff --git a/Sources/SwiftMETAR/WindsAloft/WindsAloft+Coded.swift b/Sources/SwiftMETAR/WindsAloft/WindsAloft+Coded.swift index a64fdc8..7ec933c 100644 --- a/Sources/SwiftMETAR/WindsAloft/WindsAloft+Coded.swift +++ b/Sources/SwiftMETAR/WindsAloft/WindsAloft+Coded.swift @@ -55,7 +55,7 @@ extension WindsAloft: CodedRepresentable { - Parameter coded: The coded winds aloft bulletin. - Throws: An ``Error`` if `coded` is not a valid winds aloft bulletin. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { self = try WindsAloftParser.parseSynchronously(coded) } @@ -76,7 +76,7 @@ extension WindsAloft: CodedRepresentable { } private static func dayHourMinute(_ components: DateComponents) -> String { - String( + unsafe String( format: "%02d%02d%02d", components.day ?? 0, components.hour ?? 0, @@ -85,7 +85,7 @@ extension WindsAloft: CodedRepresentable { } private static func usePeriodString(_ interval: DateComponentsInterval) -> String { - String( + unsafe String( format: "%02d%02d-%02d%02dZ", interval.start.hour ?? 0, interval.start.minute ?? 0, diff --git a/Sources/SwiftMETAR/WindsAloft/WindsAloft.swift b/Sources/SwiftMETAR/WindsAloft/WindsAloft.swift index 634dfe1..5bde8a2 100644 --- a/Sources/SwiftMETAR/WindsAloft/WindsAloft.swift +++ b/Sources/SwiftMETAR/WindsAloft/WindsAloft.swift @@ -59,7 +59,7 @@ public struct WindsAloft: Equatable, Sendable { Defaults to the current date. - Returns: A parsed ``WindsAloft`` value. */ - public static func from(string: String, on date: Date? = nil) async throws -> Self { + public static func from(string: String, on date: Date? = nil) async throws(Error) -> Self { try await WindsAloftParser.shared.parse(string, on: date) } diff --git a/Sources/SwiftMETAR/WindsAloft/WindsAloftEntry.swift b/Sources/SwiftMETAR/WindsAloft/WindsAloftEntry.swift index 4b6977e..c81fd67 100644 --- a/Sources/SwiftMETAR/WindsAloft/WindsAloftEntry.swift +++ b/Sources/SwiftMETAR/WindsAloft/WindsAloftEntry.swift @@ -82,7 +82,7 @@ public enum WindsAloftEntry: CodedRepresentable, Equatable, Sendable { dd += 50 ff -= 100 } - return String(format: "%02d%02d", Int(dd), Int(ff)) + return unsafe String(format: "%02d%02d", Int(dd), Int(ff)) + Self.codedTemperature(temperature) } } @@ -94,7 +94,7 @@ public enum WindsAloftEntry: CodedRepresentable, Equatable, Sendable { - Throws: ``Error/invalidWindsAloftGroup(_:)`` if `coded` is not a valid winds-aloft data group. */ - public init(coded: String) throws { + public init(coded: String) throws(Error) { guard let entry = try WindsAloftDataGroupParser().parse(coded) else { throw Error.invalidWindsAloftGroup(coded) } @@ -105,6 +105,6 @@ public enum WindsAloftEntry: CodedRepresentable, Equatable, Sendable { /// string when no temperature is reported. private static func codedTemperature(_ temperature: Int8?) -> String { guard let temperature else { return "" } - return String(format: "%+03d", Int(temperature)) + return unsafe String(format: "%+03d", Int(temperature)) } } diff --git a/Tests/SwiftMETARTests/WindsAloft/WindsAloftSpec.swift b/Tests/SwiftMETARTests/WindsAloft/WindsAloftSpec.swift index 10e0922..d5c8436 100644 --- a/Tests/SwiftMETARTests/WindsAloft/WindsAloftSpec.swift +++ b/Tests/SwiftMETARTests/WindsAloft/WindsAloftSpec.swift @@ -31,7 +31,7 @@ struct WindsAloftTests { /// Formats date components the way a bulletin writes them: `DDHHMM`. private static func dayHourMinute(_ components: DateComponents) -> String { - String( + unsafe String( format: "%02d%02d%02d", components.day ?? 0, components.hour ?? 0,