Skip to content

Commit fdbaa63

Browse files
RISCfutureclaude
andcommitted
Normalize Cycle interface for consistency across libraries
- Rename `current` to `effective` - Make `datum` and `cycleDuration` internal - Remove redundant `date` property (use `effectiveDate`) - Remove `endDate` (use `expirationDate` which now returns exact moment) - Add `Comparable` conformance - Add `cycle(for:)` static factory method - Move factory methods to separate extension for SwiftLint compliance - Update tests for new API and fix identical operands warning Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 4571c7c commit fdbaa63

5 files changed

Lines changed: 135 additions & 25 deletions

File tree

Sources/SwiftNASR/Cycle.swift

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ public struct Cycle: Codable, CustomStringConvertible, Sendable, Identifiable, E
99
/// The earliest reference cycle (not necessarily the earliest cycle for
1010
/// which data is available, but the earliest representable date for a
1111
/// cycle).
12-
public static let datum = Self(year: 2020, month: 12, day: 3)
12+
static let datum = Self(year: 2020, month: 12, day: 3)
1313

14-
/// `true` if this cycle's effectivity period includes the current date.
15-
public static var current: Self { effectiveCycle(for: Date())! }
14+
/// The currently effective cycle.
15+
public static var effective: Self { effectiveCycle(for: Date())! }
1616

1717
private static var calendar: Calendar {
1818
var calendar = Calendar(identifier: .gregorian)
@@ -21,7 +21,7 @@ public struct Cycle: Codable, CustomStringConvertible, Sendable, Identifiable, E
2121
}
2222

2323
/// The length of time a cycle is effective for.
24-
public static let cycleDuration: DateComponents = .init(day: 28)
24+
static let cycleDuration: DateComponents = .init(day: 28)
2525
private static let negativeCycleDuration: DateComponents = .init(day: -28)
2626

2727
/// The year of the first date of the cycle.
@@ -43,30 +43,32 @@ public struct Cycle: Codable, CustomStringConvertible, Sendable, Identifiable, E
4343
return dc
4444
}
4545

46-
/// A `Date` object representing the first effective day of this cycle.
47-
public var date: Date? { Self.calendar.date(from: dateComponents) }
46+
/// The effective date of this cycle (midnight UTC when cycle becomes effective).
47+
public var effectiveDate: Date? { Self.calendar.date(from: dateComponents) }
4848

49-
/// The last date when this cycle is effective.
50-
public var endDate: Date? {
51-
guard let date else { return nil }
52-
return Self.calendar.date(byAdding: Self.cycleDuration, to: date)
49+
/// The expiration date of this cycle (midnight UTC when cycle expires).
50+
///
51+
/// This is the exact moment the cycle expires, which is also the effective date of the next cycle.
52+
public var expirationDate: Date? {
53+
guard let effectiveDate else { return nil }
54+
return Self.calendar.date(byAdding: Self.cycleDuration, to: effectiveDate)
5355
}
5456

5557
/// The range of times when this cycle is effective.
58+
///
59+
/// The range starts at `effectiveDate` and ends at `expirationDate` (exclusive).
5660
public var dateRange: DateInterval? {
57-
guard let date else { return nil }
58-
guard let endDate else { return nil }
59-
60-
return DateInterval(start: date, end: endDate)
61+
guard let effectiveDate, let expirationDate else { return nil }
62+
return DateInterval(start: effectiveDate, end: expirationDate)
6163
}
6264

6365
/// Whether or not this cycle is currently effective.
6466
public var isEffective: Bool { contains(Date()) }
6567

6668
/// The next active cycle following this one.
6769
public var next: Self? {
68-
guard let endDate else { return nil }
69-
let components = Self.calendar.dateComponents([.year, .month, .day], from: endDate)
70+
guard let expirationDate else { return nil }
71+
let components = Self.calendar.dateComponents([.year, .month, .day], from: expirationDate)
7072
guard let year = components.year,
7173
let month = components.month,
7274
let day = components.day
@@ -76,8 +78,9 @@ public struct Cycle: Codable, CustomStringConvertible, Sendable, Identifiable, E
7678

7779
/// The previously active cycle before this one.
7880
public var previous: Self? {
79-
guard let date else { return nil }
80-
guard let prevDate = Self.calendar.date(byAdding: Self.negativeCycleDuration, to: date) else {
81+
guard let effectiveDate else { return nil }
82+
guard let prevDate = Self.calendar.date(byAdding: Self.negativeCycleDuration, to: effectiveDate)
83+
else {
8184
return nil
8285
}
8386
let components = Self.calendar.dateComponents([.year, .month, .day], from: prevDate)
@@ -116,7 +119,7 @@ public struct Cycle: Codable, CustomStringConvertible, Sendable, Identifiable, E
116119
``datum`` date.
117120
*/
118121
public static func effectiveCycle(for date: Date) -> Self? {
119-
guard var cycle = datum.date else { return nil }
122+
guard var cycle = datum.effectiveDate else { return nil }
120123
guard date >= cycle else { return nil }
121124

122125
var lastCycle = cycle
@@ -157,3 +160,23 @@ public struct Cycle: Codable, CustomStringConvertible, Sendable, Identifiable, E
157160
case year, month, day
158161
}
159162
}
163+
164+
extension Cycle: Comparable {
165+
public static func < (lhs: Cycle, rhs: Cycle) -> Bool {
166+
if lhs.year != rhs.year { return lhs.year < rhs.year }
167+
if lhs.month != rhs.month { return lhs.month < rhs.month }
168+
return lhs.day < rhs.day
169+
}
170+
}
171+
172+
// MARK: - Factory Methods
173+
174+
extension Cycle {
175+
/// Returns the cycle that contains the given date.
176+
///
177+
/// - Parameter date: The date to find the cycle for.
178+
/// - Returns: The cycle containing the date, or `nil` if the date is before the datum.
179+
public static func cycle(for date: Date) -> Self? {
180+
effectiveCycle(for: date)
181+
}
182+
}

Sources/SwiftNASR/Downloaders/ArchiveDataDownloader.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ public final class ArchiveDataDownloader: Downloader {
1313
public let session: URLSession
1414

1515
public init(cycle: Cycle? = nil, format: DataFormat = .txt) {
16-
self.cycle = cycle ?? .current
16+
self.cycle = cycle ?? .effective
1717
self.format = format
1818
session = .shared
1919
}
2020

2121
public init(cycle: Cycle? = nil, format: DataFormat = .txt, session: URLSession = .shared) {
22-
self.cycle = cycle ?? .current
22+
self.cycle = cycle ?? .effective
2323
self.format = format
2424
self.session = session
2525
}

Sources/SwiftNASR/Downloaders/ArchiveFileDownloader.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public final class ArchiveFileDownloader: Downloader {
1616
public let location: URL?
1717

1818
public init(cycle: Cycle? = nil, format: DataFormat = .txt) {
19-
self.cycle = cycle ?? .current
19+
self.cycle = cycle ?? .effective
2020
self.format = format
2121
session = .shared
2222
location = nil
@@ -28,7 +28,7 @@ public final class ArchiveFileDownloader: Downloader {
2828
location: URL? = nil,
2929
session: URLSession = .shared
3030
) {
31-
self.cycle = cycle ?? .current
31+
self.cycle = cycle ?? .effective
3232
self.format = format
3333
self.location = location
3434
self.session = session

Sources/SwiftNASR/Downloaders/Downloader.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ extension Downloader {
7676
public var cycleURL: URL {
7777
switch format {
7878
case .txt:
79-
let cycleString = cycleDateFormatter.string(from: cycle.date!)
79+
let cycleString = cycleDateFormatter.string(from: cycle.effectiveDate!)
8080
return URL(
8181
string:
8282
"https://nfdc.faa.gov/webContent/28DaySub/28DaySubscription_Effective_\(cycleString).zip"
@@ -87,7 +87,7 @@ extension Downloader {
8787
formatter.locale = Locale(identifier: "en_US")
8888
formatter.timeZone = TimeZone(identifier: "UTC")
8989
formatter.dateFormat = "dd_MMM_yyyy"
90-
let csvDateString = formatter.string(from: cycle.date!)
90+
let csvDateString = formatter.string(from: cycle.effectiveDate!)
9191
return URL(
9292
string: "https://nfdc.faa.gov/webContent/28DaySub/extra/\(csvDateString)_CSV.zip"
9393
)!

Tests/SwiftNASRTests/CycleSpec.swift

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,92 @@ final class CycleSpec: QuickSpec {
5656
expect(cycle.description).to(equal("2021-01-28"))
5757
}
5858
}
59+
60+
describe("effective") {
61+
it("returns the currently effective cycle") {
62+
let effective = Cycle.effective
63+
expect(effective.isEffective).to(beTrue())
64+
}
65+
}
66+
67+
describe("cycle(for:)") {
68+
it("returns the cycle for a given date") {
69+
let dateComponents = DateComponents(year: 2021, month: 2, day: 21)
70+
let cycle = Cycle.cycle(for: calendar.date(from: dateComponents)!)
71+
72+
expect(cycle).toNot(beNil())
73+
expect(cycle?.year).to(equal(2021))
74+
expect(cycle?.month).to(equal(1))
75+
expect(cycle?.day).to(equal(28))
76+
}
77+
78+
it("returns nil for dates before datum") {
79+
let dateComponents = DateComponents(year: 1903, month: 12, day: 17)
80+
let cycle = Cycle.cycle(for: calendar.date(from: dateComponents)!)
81+
82+
expect(cycle).to(beNil())
83+
}
84+
}
85+
86+
describe("previous and next") {
87+
var dateComponents: DateComponents { .init(year: 2021, month: 1, day: 28) }
88+
var cycle: Cycle { .effectiveCycle(for: calendar.date(from: dateComponents)!)! }
89+
90+
it("returns the previous cycle") {
91+
let previous = cycle.previous
92+
expect(previous).toNot(beNil())
93+
expect(previous?.year).to(equal(2020))
94+
expect(previous?.month).to(equal(12))
95+
expect(previous?.day).to(equal(31))
96+
}
97+
98+
it("returns the next cycle") {
99+
let next = cycle.next
100+
expect(next).toNot(beNil())
101+
expect(next?.year).to(equal(2021))
102+
expect(next?.month).to(equal(2))
103+
expect(next?.day).to(equal(25))
104+
}
105+
}
106+
107+
describe("Comparable") {
108+
it("compares cycles correctly") {
109+
let older = Cycle(year: 2021, month: 1, day: 28)
110+
let newer = Cycle(year: 2021, month: 2, day: 25)
111+
let same = Cycle(year: 2021, month: 1, day: 28)
112+
113+
expect(older < newer).to(beTrue())
114+
expect(newer > older).to(beTrue())
115+
expect(older == same).to(beTrue())
116+
}
117+
}
118+
119+
describe("dateRange") {
120+
var dateComponents: DateComponents { .init(year: 2021, month: 1, day: 28) }
121+
var cycle: Cycle { .effectiveCycle(for: calendar.date(from: dateComponents)!)! }
122+
123+
it("returns a date interval covering the cycle") {
124+
let dateRange = cycle.dateRange
125+
expect(dateRange).toNot(beNil())
126+
127+
// Duration should be 28 days
128+
let expectedDuration: TimeInterval = 28 * 24 * 60 * 60
129+
expect(dateRange?.duration).to(equal(expectedDuration))
130+
}
131+
}
132+
133+
describe("expirationDate") {
134+
var dateComponents: DateComponents { .init(year: 2021, month: 1, day: 28) }
135+
var cycle: Cycle { .effectiveCycle(for: calendar.date(from: dateComponents)!)! }
136+
137+
it("returns the exact expiration moment") {
138+
let expirationDate = cycle.expirationDate
139+
expect(expirationDate).toNot(beNil())
140+
141+
// expirationDate should equal the next cycle's effectiveDate
142+
let nextCycle = cycle.next
143+
expect(expirationDate).to(equal(nextCycle?.effectiveDate))
144+
}
145+
}
59146
}
60147
}

0 commit comments

Comments
 (0)