diff options
| -rw-r--r-- | SolverVTests/Models/SolsticeDataTests.swift | 173 | ||||
| -rw-r--r-- | Solverv/Models/SolsticeData.swift | 114 |
2 files changed, 287 insertions, 0 deletions
diff --git a/SolverVTests/Models/SolsticeDataTests.swift b/SolverVTests/Models/SolsticeDataTests.swift new file mode 100644 index 0000000..a687b6f --- /dev/null +++ b/SolverVTests/Models/SolsticeDataTests.swift @@ -0,0 +1,173 @@ +import XCTest +@testable import Solverv + +class SolsticeDataTests: XCTestCase { + + var sut: SolsticeData! + + override func setUp() { + super.setUp() + sut = SolsticeData.shared + } + + override func tearDown() { + sut = nil + super.tearDown() + } + + // MARK: - nextEvent() Tests + + func testNextEventReturnsValidEvent() { + let nextEvent = sut.nextEvent() + + // Should return a valid event + XCTAssertNotNil(nextEvent, "nextEvent() should return a valid event") + + // The next event should be in the future + if let nextEvent = nextEvent { + XCTAssertGreaterThan(nextEvent.date, Date(), "Next event should be in the future") + } + } + + func testNextEventDateIsAfterCurrentDate() { + guard let nextEvent = sut.nextEvent() else { + XCTFail("nextEvent() should not be nil") + return + } + + let now = Date() + XCTAssertGreaterThan(nextEvent.date, now, "Next event date should be after current date") + } + + // MARK: - upcomingEvents(count:) Tests + + func testUpcomingEventsReturnsCorrectCount() { + let count = 3 + let upcomingEvents = sut.upcomingEvents(count: count) + + XCTAssertLessThanOrEqual(upcomingEvents.count, count, "Should return at most \(count) events") + } + + func testUpcomingEventsReturnsEventsInOrder() { + let upcomingEvents = sut.upcomingEvents(count: 5) + + // All events should be in the future + let now = Date() + for event in upcomingEvents { + XCTAssertGreaterThan(event.date, now, "All upcoming events should be in the future") + } + + // Events should be in chronological order + for i in 1..<upcomingEvents.count { + XCTAssertLessThan(upcomingEvents[i - 1].date, upcomingEvents[i].date, + "Events should be in chronological order") + } + } + + func testUpcomingEventsWithZeroCount() { + let upcomingEvents = sut.upcomingEvents(count: 0) + XCTAssertEqual(upcomingEvents.count, 0, "Should return empty array when count is 0") + } + + func testUpcomingEventsWithLargeCount() { + let upcomingEvents = sut.upcomingEvents(count: 100) + + // Should return events but not more than available + XCTAssertGreaterThan(upcomingEvents.count, 0, "Should return some events") + XCTAssertLessThanOrEqual(upcomingEvents.count, 100, "Should not exceed requested count") + } + + // MARK: - progressToNextEvent() Tests + + func testProgressToNextEventReturnsValidRatio() { + guard let progress = sut.progressToNextEvent() else { + XCTFail("progressToNextEvent() should return a valid progress") + return + } + + // Elapsed should be >= 0 + XCTAssertGreaterThanOrEqual(progress.elapsed, 0, "Elapsed days should be >= 0") + + // Total should be > 0 + XCTAssertGreaterThan(progress.total, 0, "Total days should be > 0") + + // Elapsed should be <= total + XCTAssertLessThanOrEqual(progress.elapsed, progress.total, "Elapsed should not exceed total") + } + + func testProgressBarCalculationExample() { + // This tests the "31/89" example from the specification + guard let progress = sut.progressToNextEvent() else { + XCTFail("progressToNextEvent() should return a valid progress") + return + } + + // Verify the format is correct: (elapsed, total) + let elapsedStr = String(progress.elapsed) + let totalStr = String(progress.total) + let progressStr = "\(elapsedStr)/\(totalStr)" + + // Just verify the format is correct + XCTAssertFalse(progressStr.isEmpty, "Progress string should not be empty") + XCTAssertTrue(progressStr.contains("/"), "Progress string should contain '/'") + } + + // MARK: - Data Integrity Tests + + func testAllEventsAreUniqueAndOrdered() { + let allEvents = sut.upcomingEvents(count: 1000) // Get as many as available + + // Check that events are in order + for i in 1..<allEvents.count { + XCTAssertLessThan(allEvents[i - 1].date, allEvents[i].date, + "Events should be in chronological order") + } + } + + func testEventsHaveValidSeasons() { + let allEvents = sut.upcomingEvents(count: 1000) + + for event in allEvents { + // Season should match the date + let month = Calendar.current.component(.month, from: event.date) + switch month { + case 3, 4, 5: + XCTAssertEqual(event.season, .spring, "March-May should be spring") + case 6, 7, 8: + XCTAssertEqual(event.season, .summer, "June-August should be summer") + case 9, 10, 11: + XCTAssertEqual(event.season, .autumn, "September-November should be autumn") + default: + XCTAssertEqual(event.season, .winter, "December, January, February should be winter") + } + } + } + + func testSingletonPattern() { + let instance1 = SolsticeData.shared + let instance2 = SolsticeData.shared + + // Both should reference the same object + XCTAssertTrue(instance1 === instance2, "Singleton should return the same instance") + } + + func testEventNamesAreNotEmpty() { + let allEvents = sut.upcomingEvents(count: 1000) + + for event in allEvents { + XCTAssertFalse(event.name.isEmpty, "Event name should not be empty") + } + } + + func testEventDatesAreValid() { + let allEvents = sut.upcomingEvents(count: 1000) + + for event in allEvents { + // Event should have a valid date + let year = Calendar.current.component(.year, from: event.date) + XCTAssertGreaterThanOrEqual(year, 2025, "Event year should be 2025 or later") + XCTAssertLessThanOrEqual(year, 2030, "Event year should be 2030 or earlier") + } + } + +} diff --git a/Solverv/Models/SolsticeData.swift b/Solverv/Models/SolsticeData.swift new file mode 100644 index 0000000..5a36da7 --- /dev/null +++ b/Solverv/Models/SolsticeData.swift @@ -0,0 +1,114 @@ +import Foundation + +class SolsticeData { + static let shared = SolsticeData() + + private let events: [SolsticeEvent] + + private init() { + // Hardcoded solstice/equinox events for 2025-2030 (all in UTC) + var allEvents: [SolsticeEvent] = [] + + // 2025 + allEvents.append(SolsticeEvent(name: "Spring Equinox 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 3, day: 20, hour: 9, minute: 1), season: .spring)) + allEvents.append(SolsticeEvent(name: "Summer Solstice 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 6, day: 20, hour: 14, minute: 42), season: .summer)) + allEvents.append(SolsticeEvent(name: "Autumn Equinox 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 9, day: 22, hour: 18, minute: 20), season: .autumn)) + allEvents.append(SolsticeEvent(name: "Winter Solstice 2025", date: SolsticeData.dateFromUTC(year: 2025, month: 12, day: 21, hour: 15, minute: 3), season: .winter)) + + // 2026 + allEvents.append(SolsticeEvent(name: "Spring Equinox 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 3, day: 20, hour: 14, minute: 46), season: .spring)) + allEvents.append(SolsticeEvent(name: "Summer Solstice 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 6, day: 21, hour: 8, minute: 25), season: .summer)) + allEvents.append(SolsticeEvent(name: "Autumn Equinox 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 9, day: 23, hour: 0, minute: 6), season: .autumn)) + allEvents.append(SolsticeEvent(name: "Winter Solstice 2026", date: SolsticeData.dateFromUTC(year: 2026, month: 12, day: 21, hour: 20, minute: 50), season: .winter)) + + // 2027 + allEvents.append(SolsticeEvent(name: "Spring Equinox 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 3, day: 20, hour: 20, minute: 25), season: .spring)) + allEvents.append(SolsticeEvent(name: "Summer Solstice 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 6, day: 21, hour: 14, minute: 11), season: .summer)) + allEvents.append(SolsticeEvent(name: "Autumn Equinox 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 9, day: 23, hour: 6, minute: 2), season: .autumn)) + allEvents.append(SolsticeEvent(name: "Winter Solstice 2027", date: SolsticeData.dateFromUTC(year: 2027, month: 12, day: 22, hour: 2, minute: 43), season: .winter)) + + // 2028 + allEvents.append(SolsticeEvent(name: "Spring Equinox 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 3, day: 20, hour: 2, minute: 17), season: .spring)) + allEvents.append(SolsticeEvent(name: "Summer Solstice 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 6, day: 20, hour: 20, minute: 2), season: .summer)) + allEvents.append(SolsticeEvent(name: "Autumn Equinox 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 9, day: 22, hour: 11, minute: 45), season: .autumn)) + allEvents.append(SolsticeEvent(name: "Winter Solstice 2028", date: SolsticeData.dateFromUTC(year: 2028, month: 12, day: 21, hour: 8, minute: 20), season: .winter)) + + // 2029 + allEvents.append(SolsticeEvent(name: "Spring Equinox 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 3, day: 20, hour: 8, minute: 1), season: .spring)) + allEvents.append(SolsticeEvent(name: "Summer Solstice 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 6, day: 21, hour: 1, minute: 48), season: .summer)) + allEvents.append(SolsticeEvent(name: "Autumn Equinox 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 9, day: 22, hour: 17, minute: 37), season: .autumn)) + allEvents.append(SolsticeEvent(name: "Winter Solstice 2029", date: SolsticeData.dateFromUTC(year: 2029, month: 12, day: 21, hour: 14, minute: 14), season: .winter)) + + // 2030 + allEvents.append(SolsticeEvent(name: "Spring Equinox 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 3, day: 20, hour: 13, minute: 51), season: .spring)) + allEvents.append(SolsticeEvent(name: "Summer Solstice 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 6, day: 21, hour: 7, minute: 31), season: .summer)) + allEvents.append(SolsticeEvent(name: "Autumn Equinox 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 9, day: 22, hour: 23, minute: 27), season: .autumn)) + allEvents.append(SolsticeEvent(name: "Winter Solstice 2030", date: SolsticeData.dateFromUTC(year: 2030, month: 12, day: 21, hour: 20, minute: 9), season: .winter)) + + // Sort events by date + self.events = allEvents.sorted { $0.date < $1.date } + } + + /// Helper function to create UTC dates (static to be usable during init) + private static func dateFromUTC(year: Int, month: Int, day: Int, hour: Int, minute: Int) -> Date { + var components = DateComponents() + components.year = year + components.month = month + components.day = day + components.hour = hour + components.minute = minute + components.second = 0 + components.timeZone = TimeZone(abbreviation: "UTC") + + return Calendar(identifier: .gregorian).date(from: components) ?? Date() + } + + /// Returns the next upcoming solstice/equinox event + func nextEvent() -> SolsticeEvent? { + let now = Date() + return events.first { $0.date > now } + } + + /// Returns the next N upcoming events + func upcomingEvents(count: Int) -> [SolsticeEvent] { + let now = Date() + let futureEvents = events.filter { $0.date > now } + return Array(futureEvents.prefix(count)) + } + + /// Returns the progress to the next event as (elapsed days, total days) + func progressToNextEvent() -> (elapsed: Int, total: Int)? { + guard let nextEvent = nextEvent() else { + return nil + } + + let now = Date() + let today = Calendar.current.startOfDay(for: now) + let eventDay = Calendar.current.startOfDay(for: nextEvent.date) + + // Find the previous event to calculate total days + guard let previousEventIndex = events.firstIndex(where: { $0.date > now }) else { + return nil + } + + let previousEvent: Date + if previousEventIndex > 0 { + previousEvent = events[previousEventIndex - 1].date + } else { + // If this is the first event, we need to handle this case + // Use the event itself minus some arbitrary period (not applicable for first event) + return nil + } + + let previousEventDay = Calendar.current.startOfDay(for: previousEvent) + + // Calculate elapsed and total days + let elapsedComponents = Calendar.current.dateComponents([.day], from: previousEventDay, to: today) + let totalComponents = Calendar.current.dateComponents([.day], from: previousEventDay, to: eventDay) + + let elapsed = max(0, elapsedComponents.day ?? 0) + let total = max(1, totalComponents.day ?? 1) + + return (elapsed: elapsed, total: total) + } +} |
