From def072548492755a00294f3645f65cdd947dba9e Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Fri, 29 Dec 2023 09:18:09 -0500 Subject: [PATCH 01/15] [Rename] `KCAstronomicalCalculatorTests.swift` to `KCNOAACalculatorTests.swift`. --- KosherCocoa.xcodeproj/project.pbxproj | 8 ++++---- ...ests.swift => KCNOAACalculatorTests.swift} | 20 ++++++++++--------- 2 files changed, 15 insertions(+), 13 deletions(-) rename KosherCocoaTests/{KCAstronomicalCalculatorTests.swift => KCNOAACalculatorTests.swift} (95%) diff --git a/KosherCocoa.xcodeproj/project.pbxproj b/KosherCocoa.xcodeproj/project.pbxproj index f5e3ab3..c210892 100644 --- a/KosherCocoa.xcodeproj/project.pbxproj +++ b/KosherCocoa.xcodeproj/project.pbxproj @@ -75,7 +75,7 @@ 4618EE1C1BF4F9F900C33159 /* NSDateComponents+AllComponents.m in Sources */ = {isa = PBXBuildFile; fileRef = 466A3ACE17CAF3B900056F16 /* NSDateComponents+AllComponents.m */; }; 469066561F4A7090009E5D46 /* KosherCocoa.podspec in Resources */ = {isa = PBXBuildFile; fileRef = 469066551F4A7090009E5D46 /* KosherCocoa.podspec */; }; 46DB49691D9839F200F3A576 /* KosherCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4618ECE51BF449EF00C33159 /* KosherCocoa.framework */; }; - 5D435CEA2A3C34CB00F66AE0 /* KCAstronomicalCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D435CE92A3C34CB00F66AE0 /* KCAstronomicalCalculatorTests.swift */; }; + 5D435CEA2A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D435CE92A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift */; }; 5D435CEC2A3C352500F66AE0 /* KCNOAACalculator.h in Headers */ = {isa = PBXBuildFile; fileRef = 5D435CEB2A3C352500F66AE0 /* KCNOAACalculator.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5D435CEF2A3C35B000F66AE0 /* KCNOAACalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D435CEE2A3C35B000F66AE0 /* KCNOAACalculator.m */; }; 5D435CF22A3C369900F66AE0 /* KCAstronomicalCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D435CF12A3C369900F66AE0 /* KCAstronomicalCalculator.m */; }; @@ -174,7 +174,7 @@ 46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KosherCocoaMetadataTests.swift; sourceTree = ""; }; 46DB495C1D9837D100F3A576 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 46DB49641D9839F100F3A576 /* KosherCocoaTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = KosherCocoaTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 5D435CE92A3C34CB00F66AE0 /* KCAstronomicalCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCAstronomicalCalculatorTests.swift; sourceTree = ""; }; + 5D435CE92A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCNOAACalculatorTests.swift; sourceTree = ""; }; 5D435CEB2A3C352500F66AE0 /* KCNOAACalculator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KCNOAACalculator.h; sourceTree = ""; }; 5D435CEE2A3C35B000F66AE0 /* KCNOAACalculator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KCNOAACalculator.m; sourceTree = ""; }; 5D435CF12A3C369900F66AE0 /* KCAstronomicalCalculator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KCAstronomicalCalculator.m; sourceTree = ""; }; @@ -454,7 +454,7 @@ 46DB49591D9837D100F3A576 /* KosherCocoaTests */ = { isa = PBXGroup; children = ( - 5D435CE92A3C34CB00F66AE0 /* KCAstronomicalCalculatorTests.swift */, + 5D435CE92A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift */, 46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */, F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */, 5D435CF32A3C389B00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png */, @@ -656,7 +656,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 5D435CEA2A3C34CB00F66AE0 /* KCAstronomicalCalculatorTests.swift in Sources */, + 5D435CEA2A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift in Sources */, F5F902FA1FC53B17003FE90D /* KosherCocoaMetadataTests.swift in Sources */, F5F902FC1FC53C01003FE90D /* KCJewishCalendarTests.swift in Sources */, ); diff --git a/KosherCocoaTests/KCAstronomicalCalculatorTests.swift b/KosherCocoaTests/KCNOAACalculatorTests.swift similarity index 95% rename from KosherCocoaTests/KCAstronomicalCalculatorTests.swift rename to KosherCocoaTests/KCNOAACalculatorTests.swift index 31d4aae..6acd917 100644 --- a/KosherCocoaTests/KCAstronomicalCalculatorTests.swift +++ b/KosherCocoaTests/KCNOAACalculatorTests.swift @@ -1,16 +1,18 @@ -// -// KCAstronomicalCalculatorTests.swift -// KosherCocoaTests -// -// Created by Elyahu on 2/5/23. -// Copyright © 2023 Moshe Berman. All rights reserved. -// +/** + * KCNOAACalculatorTests + * KosherCocoa 3 + * + * Created by Elyahu on 2/5/23. + * Updated by Moshe Berman on 12/29/23. + * + * Use of KosherCocoa 3 is governed by the LGPL 2.1 License. + */ import XCTest import KosherCocoa -final class KCAstronomicalCalculatorTests: XCTestCase { - +final class KCNOAACalculatorTests: XCTestCase { + let gregorianCalendar = Calendar(identifier: .gregorian) override func setUpWithError() throws { From 70cea51c0943e3cd793723dc0e57b25c9a9537d1 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Fri, 29 Dec 2023 09:19:52 -0500 Subject: [PATCH 02/15] Add file `KCAstronomicalCalculatorTests.swift`. --- KosherCocoa.xcodeproj/project.pbxproj | 4 +++ .../KCAstronomicalCalculatorTests.swift | 36 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 KosherCocoaTests/KCAstronomicalCalculatorTests.swift diff --git a/KosherCocoa.xcodeproj/project.pbxproj b/KosherCocoa.xcodeproj/project.pbxproj index c210892..d0906e8 100644 --- a/KosherCocoa.xcodeproj/project.pbxproj +++ b/KosherCocoa.xcodeproj/project.pbxproj @@ -81,6 +81,7 @@ 5D435CF22A3C369900F66AE0 /* KCAstronomicalCalculator.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D435CF12A3C369900F66AE0 /* KCAstronomicalCalculator.m */; }; 5D435CF42A3C389C00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png in Resources */ = {isa = PBXBuildFile; fileRef = 5D435CF32A3C389B00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png */; }; F52587431ECD019A009E623C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F52587411ECD019A009E623C /* Localizable.strings */; }; + F5C6076F2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */; }; F5F902FA1FC53B17003FE90D /* KosherCocoaMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */; }; F5F902FC1FC53C01003FE90D /* KCJewishCalendarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */; }; /* End PBXBuildFile section */ @@ -180,6 +181,7 @@ 5D435CF12A3C369900F66AE0 /* KCAstronomicalCalculator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KCAstronomicalCalculator.m; sourceTree = ""; }; 5D435CF32A3C389B00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Screenshot 2023-06-16 at 2.27.26 AM.png"; sourceTree = ""; }; F52587421ECD019A009E623C /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = Localizations/en.lproj/Localizable.strings; sourceTree = ""; }; + F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCAstronomicalCalculatorTests.swift; sourceTree = ""; }; F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCJewishCalendarTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -455,6 +457,7 @@ isa = PBXGroup; children = ( 5D435CE92A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift */, + F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */, 46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */, F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */, 5D435CF32A3C389B00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png */, @@ -657,6 +660,7 @@ buildActionMask = 2147483647; files = ( 5D435CEA2A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift in Sources */, + F5C6076F2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift in Sources */, F5F902FA1FC53B17003FE90D /* KosherCocoaMetadataTests.swift in Sources */, F5F902FC1FC53C01003FE90D /* KCJewishCalendarTests.swift in Sources */, ); diff --git a/KosherCocoaTests/KCAstronomicalCalculatorTests.swift b/KosherCocoaTests/KCAstronomicalCalculatorTests.swift new file mode 100644 index 0000000..ced7028 --- /dev/null +++ b/KosherCocoaTests/KCAstronomicalCalculatorTests.swift @@ -0,0 +1,36 @@ +// +// KCAstronomicalCalculatorTests.swift +// KosherCocoaTests +// +// Created by Moshe Berman on 12/29/23. +// Copyright © 2023 Moshe Berman. All rights reserved. +// + +import XCTest + +final class KCAstronomicalCalculatorTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} From 99e48ebeb9602ce2477b835b1c46490c5eb7f455 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Fri, 29 Dec 2023 09:22:28 -0500 Subject: [PATCH 03/15] Split `testCalculatorNames` between `KCNOAACalculatorTests` and `KCAstronomicalCalculatorTests`. --- .../KCAstronomicalCalculatorTests.swift | 17 ++++------------- KosherCocoaTests/KCNOAACalculatorTests.swift | 3 --- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/KosherCocoaTests/KCAstronomicalCalculatorTests.swift b/KosherCocoaTests/KCAstronomicalCalculatorTests.swift index ced7028..ca5c0f2 100644 --- a/KosherCocoaTests/KCAstronomicalCalculatorTests.swift +++ b/KosherCocoaTests/KCAstronomicalCalculatorTests.swift @@ -7,6 +7,7 @@ // import XCTest +import KosherCocoa final class KCAstronomicalCalculatorTests: XCTestCase { @@ -18,19 +19,9 @@ final class KCAstronomicalCalculatorTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. } - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - // Any test you write for XCTest can be annotated as throws and async. - // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. - // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. - } - - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. - } + func testCalculatorNames() throws { + let calculator = SunriseAndSunsetCalculator(geoLocation: GeoLocation()) + XCTAssertEqual(calculator.calculatorName, "United States Naval Almanac Algorithm") } } diff --git a/KosherCocoaTests/KCNOAACalculatorTests.swift b/KosherCocoaTests/KCNOAACalculatorTests.swift index 6acd917..19fba36 100644 --- a/KosherCocoaTests/KCNOAACalculatorTests.swift +++ b/KosherCocoaTests/KCNOAACalculatorTests.swift @@ -26,9 +26,6 @@ final class KCNOAACalculatorTests: XCTestCase { func testCalculatorNames() throws { let calculator = NOAACalculator(geoLocation: GeoLocation()) XCTAssertEqual(calculator.calculatorName, "US National Oceanic and Atmospheric Administration Algorithm") - - let SunriseAndSunsetCalculator = SunriseAndSunsetCalculator(geoLocation: GeoLocation()) - XCTAssertEqual(SunriseAndSunsetCalculator.calculatorName, "United States Naval Almanac Algorithm") } func testCalculatorSunrise() throws { From 75584931075a558a55c7245b6f81048fe27604e9 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Fri, 29 Dec 2023 09:27:10 -0500 Subject: [PATCH 04/15] Extract lakewood calculator to an instance variable. --- KosherCocoaTests/KCNOAACalculatorTests.swift | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/KosherCocoaTests/KCNOAACalculatorTests.swift b/KosherCocoaTests/KCNOAACalculatorTests.swift index 19fba36..f3c0e99 100644 --- a/KosherCocoaTests/KCNOAACalculatorTests.swift +++ b/KosherCocoaTests/KCNOAACalculatorTests.swift @@ -14,6 +14,14 @@ import KosherCocoa final class KCNOAACalculatorTests: XCTestCase { let gregorianCalendar = Calendar(identifier: .gregorian) + let lakewood: GeoLocation = GeoLocation( + latitude: 40.08213, + andLongitude: -74.20970, + andTimeZone: TimeZone(identifier: "America/New_York")! + ) + lazy var lakewoodCalculator:NOAACalculator = { + NOAACalculator(geoLocation: lakewood) + }() override func setUpWithError() throws { try super.setUpWithError() @@ -24,13 +32,12 @@ final class KCNOAACalculatorTests: XCTestCase { } func testCalculatorNames() throws { - let calculator = NOAACalculator(geoLocation: GeoLocation()) - XCTAssertEqual(calculator.calculatorName, "US National Oceanic and Atmospheric Administration Algorithm") + XCTAssertEqual(lakewoodCalculator.calculatorName, "US National Oceanic and Atmospheric Administration Algorithm") } func testCalculatorSunrise() throws { - let lakewoodCalculator = NOAACalculator(geoLocation: GeoLocation(latitude: 40.08213, andLongitude: -74.20970, andTimeZone: TimeZone(identifier: "America/New_York")!)) - + + var januaryFirst = DateComponents() januaryFirst.year = 2023 januaryFirst.month = 1 From 3beee2efcb95a95c14d3054c7270099f9fc40bed Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Fri, 29 Dec 2023 09:40:27 -0500 Subject: [PATCH 05/15] Extract astronomical calendar to an instance variable. --- KosherCocoaTests/KCNOAACalculatorTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/KosherCocoaTests/KCNOAACalculatorTests.swift b/KosherCocoaTests/KCNOAACalculatorTests.swift index f3c0e99..39e4d0b 100644 --- a/KosherCocoaTests/KCNOAACalculatorTests.swift +++ b/KosherCocoaTests/KCNOAACalculatorTests.swift @@ -23,6 +23,10 @@ final class KCNOAACalculatorTests: XCTestCase { NOAACalculator(geoLocation: lakewood) }() + lazy var calendar: AstronomicalCalendar = { + AstronomicalCalendar(location: lakewoodCalculator.geoLocation) + }() + override func setUpWithError() throws { try super.setUpWithError() } @@ -36,8 +40,6 @@ final class KCNOAACalculatorTests: XCTestCase { } func testCalculatorSunrise() throws { - - var januaryFirst = DateComponents() januaryFirst.year = 2023 januaryFirst.month = 1 From 0d26ca88530e6e3b7109296b6cbfd48fd6262697 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Fri, 29 Dec 2023 10:16:43 -0500 Subject: [PATCH 06/15] Add file containing test cases for NOAA sunrise and sunset.. --- KosherCocoa.xcodeproj/project.pbxproj | 4 + .../SunriseSunsetLakewoodNOAA.csv | 366 ++++++++++++++++++ 2 files changed, 370 insertions(+) create mode 100644 KosherCocoaTests/SunriseSunsetLakewoodNOAA.csv diff --git a/KosherCocoa.xcodeproj/project.pbxproj b/KosherCocoa.xcodeproj/project.pbxproj index d0906e8..22dddf0 100644 --- a/KosherCocoa.xcodeproj/project.pbxproj +++ b/KosherCocoa.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ 5D435CF42A3C389C00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png in Resources */ = {isa = PBXBuildFile; fileRef = 5D435CF32A3C389B00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png */; }; F52587431ECD019A009E623C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F52587411ECD019A009E623C /* Localizable.strings */; }; F5C6076F2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */; }; + F5C607722B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv in Resources */ = {isa = PBXBuildFile; fileRef = F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */; }; F5F902FA1FC53B17003FE90D /* KosherCocoaMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */; }; F5F902FC1FC53C01003FE90D /* KCJewishCalendarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */; }; /* End PBXBuildFile section */ @@ -182,6 +183,7 @@ 5D435CF32A3C389B00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "Screenshot 2023-06-16 at 2.27.26 AM.png"; sourceTree = ""; }; F52587421ECD019A009E623C /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = Localizations/en.lproj/Localizable.strings; sourceTree = ""; }; F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCAstronomicalCalculatorTests.swift; sourceTree = ""; }; + F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SunriseSunsetLakewoodNOAA.csv; sourceTree = ""; }; F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCJewishCalendarTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -456,6 +458,7 @@ 46DB49591D9837D100F3A576 /* KosherCocoaTests */ = { isa = PBXGroup; children = ( + F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */, 5D435CE92A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift */, F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */, 46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */, @@ -614,6 +617,7 @@ buildActionMask = 2147483647; files = ( 5D435CF42A3C389C00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png in Resources */, + F5C607722B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/KosherCocoaTests/SunriseSunsetLakewoodNOAA.csv b/KosherCocoaTests/SunriseSunsetLakewoodNOAA.csv new file mode 100644 index 0000000..ec22258 --- /dev/null +++ b/KosherCocoaTests/SunriseSunsetLakewoodNOAA.csv @@ -0,0 +1,366 @@ +Date, sunrise,seaLevelSunrise,sunset,seaLevelSunset +"Jan 1, 2023",7:18:03 AM,7:19:02 AM,4:42:56 PM,4:41:57 PM +"Jan 2, 2023",7:18:10 AM,7:19:09 AM,4:43:46 PM,4:42:47 PM +"Jan 3, 2023",7:18:14 AM,7:19:13 AM,4:44:38 PM,4:43:39 PM +"Jan 4, 2023",7:18:16 AM,7:19:15 AM,4:45:31 PM,4:44:33 PM +"Jan 5, 2023",7:18:16 AM,7:19:15 AM,4:46:26 PM,4:45:27 PM +"Jan 6, 2023",7:18:14 AM,7:19:12 AM,4:47:22 PM,4:46:23 PM +"Jan 7, 2023",7:18:09 AM,7:19:07 AM,4:48:19 PM,4:47:21 PM +"Jan 8, 2023",7:18:02 AM,7:19:01 AM,4:49:18 PM,4:48:19 PM +"Jan 9, 2023",7:17:53 AM,7:18:52 AM,4:50:17 PM,4:49:19 PM +"Jan 10, 2023",7:17:42 AM,7:18:40 AM,4:51:18 PM,4:50:20 PM +"Jan 11, 2023",7:17:29 AM,7:18:27 AM,4:52:20 PM,4:51:22 PM +"Jan 12, 2023",7:17:13 AM,7:18:11 AM,4:53:23 PM,4:52:25 PM +"Jan 13, 2023",7:16:56 AM,7:17:53 AM,4:54:27 PM,4:53:29 PM +"Jan 14, 2023",7:16:36 AM,7:17:33 AM,4:55:31 PM,4:54:34 PM +"Jan 15, 2023",7:16:14 AM,7:17:11 AM,4:56:37 PM,4:55:40 PM +"Jan 16, 2023",7:15:49 AM,7:16:47 AM,4:57:43 PM,4:56:46 PM +"Jan 17, 2023",7:15:23 AM,7:16:20 AM,4:58:51 PM,4:57:53 PM +"Jan 18, 2023",7:14:55 AM,7:15:52 AM,4:59:58 PM,4:59:01 PM +"Jan 19, 2023",7:14:24 AM,7:15:21 AM,5:01:07 PM,5:00:10 PM +"Jan 20, 2023",7:13:52 AM,7:14:48 AM,5:02:16 PM,5:01:19 PM +"Jan 21, 2023",7:13:17 AM,7:14:14 AM,5:03:25 PM,5:02:29 PM +"Jan 22, 2023",7:12:40 AM,7:13:37 AM,5:04:35 PM,5:03:39 PM +"Jan 23, 2023",7:12:02 AM,7:12:58 AM,5:05:46 PM,5:04:50 PM +"Jan 24, 2023",7:11:21 AM,7:12:17 AM,5:06:57 PM,5:06:01 PM +"Jan 25, 2023",7:10:39 AM,7:11:35 AM,5:08:08 PM,5:07:12 PM +"Jan 26, 2023",7:09:54 AM,7:10:50 AM,5:09:19 PM,5:08:24 PM +"Jan 27, 2023",7:09:08 AM,7:10:04 AM,5:10:31 PM,5:09:36 PM +"Jan 28, 2023",7:08:20 AM,7:09:16 AM,5:11:43 PM,5:10:48 PM +"Jan 29, 2023",7:07:30 AM,7:08:25 AM,5:12:55 PM,5:12:00 PM +"Jan 30, 2023",7:06:38 AM,7:07:34 AM,5:14:08 PM,5:13:13 PM +"Jan 31, 2023",7:05:45 AM,7:06:40 AM,5:15:20 PM,5:14:25 PM +"Feb 1, 2023",7:04:50 AM,7:05:45 AM,5:16:33 PM,5:15:38 PM +"Feb 2, 2023",7:03:53 AM,7:04:48 AM,5:17:45 PM,5:16:50 PM +"Feb 3, 2023",7:02:54 AM,7:03:49 AM,5:18:58 PM,5:18:03 PM +"Feb 4, 2023",7:01:54 AM,7:02:49 AM,5:20:10 PM,5:19:16 PM +"Feb 5, 2023",7:00:52 AM,7:01:47 AM,5:21:23 PM,5:20:28 PM +"Feb 6, 2023",6:59:49 AM,7:00:43 AM,5:22:35 PM,5:21:41 PM +"Feb 7, 2023",6:58:44 AM,6:59:38 AM,5:23:47 PM,5:22:53 PM +"Feb 8, 2023",6:57:38 AM,6:58:32 AM,5:24:59 PM,5:24:05 PM +"Feb 9, 2023",6:56:30 AM,6:57:24 AM,5:26:11 PM,5:25:17 PM +"Feb 10, 2023",6:55:21 AM,6:56:15 AM,5:27:23 PM,5:26:29 PM +"Feb 11, 2023",6:54:10 AM,6:55:04 AM,5:28:35 PM,5:27:41 PM +"Feb 12, 2023",6:52:58 AM,6:53:52 AM,5:29:46 PM,5:28:53 PM +"Feb 13, 2023",6:51:45 AM,6:52:38 AM,5:30:57 PM,5:30:04 PM +"Feb 14, 2023",6:50:31 AM,6:51:24 AM,5:32:08 PM,5:31:15 PM +"Feb 15, 2023",6:49:15 AM,6:50:08 AM,5:33:19 PM,5:32:26 PM +"Feb 16, 2023",6:47:58 AM,6:48:51 AM,5:34:29 PM,5:33:36 PM +"Feb 17, 2023",6:46:40 AM,6:47:33 AM,5:35:39 PM,5:34:47 PM +"Feb 18, 2023",6:45:21 AM,6:46:13 AM,5:36:49 PM,5:35:56 PM +"Feb 19, 2023",6:44:00 AM,6:44:53 AM,5:37:59 PM,5:37:06 PM +"Feb 20, 2023",6:42:39 AM,6:43:31 AM,5:39:08 PM,5:38:16 PM +"Feb 21, 2023",6:41:16 AM,6:42:09 AM,5:40:17 PM,5:39:25 PM +"Feb 22, 2023",6:39:53 AM,6:40:45 AM,5:41:26 PM,5:40:33 PM +"Feb 23, 2023",6:38:28 AM,6:39:21 AM,5:42:34 PM,5:41:42 PM +"Feb 24, 2023",6:37:03 AM,6:37:55 AM,5:43:42 PM,5:42:50 PM +"Feb 25, 2023",6:35:37 AM,6:36:29 AM,5:44:50 PM,5:43:58 PM +"Feb 26, 2023",6:34:10 AM,6:35:02 AM,5:45:57 PM,5:45:05 PM +"Feb 27, 2023",6:32:42 AM,6:33:34 AM,5:47:05 PM,5:46:13 PM +"Feb 28, 2023",6:31:13 AM,6:32:05 AM,5:48:11 PM,5:47:20 PM +"Mar 1, 2023",6:29:44 AM,6:30:35 AM,5:49:18 PM,5:48:26 PM +"Mar 2, 2023",6:28:14 AM,6:29:05 AM,5:50:24 PM,5:49:33 PM +"Mar 3, 2023",6:26:43 AM,6:27:34 AM,5:51:30 PM,5:50:39 PM +"Mar 4, 2023",6:25:11 AM,6:26:03 AM,5:52:36 PM,5:51:44 PM +"Mar 5, 2023",6:23:39 AM,6:24:31 AM,5:53:41 PM,5:52:50 PM +"Mar 6, 2023",6:22:07 AM,6:22:58 AM,5:54:46 PM,5:53:55 PM +"Mar 7, 2023",6:20:34 AM,6:21:25 AM,5:55:51 PM,5:55:00 PM +"Mar 8, 2023",6:19:00 AM,6:19:51 AM,5:56:56 PM,5:56:05 PM +"Mar 9, 2023",6:17:26 AM,6:18:17 AM,5:58:00 PM,5:57:09 PM +"Mar 10, 2023",6:15:51 AM,6:16:42 AM,5:59:04 PM,5:58:13 PM +"Mar 11, 2023",6:14:16 AM,6:15:07 AM,6:00:08 PM,5:59:17 PM +"Mar 12, 2023",7:12:40 AM,7:13:31 AM,7:01:12 PM,7:00:21 PM +"Mar 13, 2023",7:11:04 AM,7:11:56 AM,7:02:15 PM,7:01:24 PM +"Mar 14, 2023",7:09:28 AM,7:10:19 AM,7:03:19 PM,7:02:27 PM +"Mar 15, 2023",7:07:52 AM,7:08:43 AM,7:04:22 PM,7:03:30 PM +"Mar 16, 2023",7:06:15 AM,7:07:06 AM,7:05:24 PM,7:04:33 PM +"Mar 17, 2023",7:04:38 AM,7:05:29 AM,7:06:27 PM,7:05:36 PM +"Mar 18, 2023",7:03:01 AM,7:03:52 AM,7:07:30 PM,7:06:39 PM +"Mar 19, 2023",7:01:24 AM,7:02:15 AM,7:08:32 PM,7:07:41 PM +"Mar 20, 2023",6:59:46 AM,7:00:37 AM,7:09:34 PM,7:08:43 PM +"Mar 21, 2023",6:58:09 AM,6:59:00 AM,7:10:36 PM,7:09:45 PM +"Mar 22, 2023",6:56:31 AM,6:57:22 AM,7:11:38 PM,7:10:47 PM +"Mar 23, 2023",6:54:53 AM,6:55:44 AM,7:12:40 PM,7:11:49 PM +"Mar 24, 2023",6:53:16 AM,6:54:07 AM,7:13:42 PM,7:12:51 PM +"Mar 25, 2023",6:51:38 AM,6:52:29 AM,7:14:43 PM,7:13:52 PM +"Mar 26, 2023",6:50:00 AM,6:50:51 AM,7:15:45 PM,7:14:54 PM +"Mar 27, 2023",6:48:23 AM,6:49:14 AM,7:16:46 PM,7:15:55 PM +"Mar 28, 2023",6:46:45 AM,6:47:36 AM,7:17:48 PM,7:16:57 PM +"Mar 29, 2023",6:45:08 AM,6:45:59 AM,7:18:49 PM,7:17:58 PM +"Mar 30, 2023",6:43:31 AM,6:44:22 AM,7:19:50 PM,7:18:59 PM +"Mar 31, 2023",6:41:54 AM,6:42:45 AM,7:20:52 PM,7:20:00 PM +"Apr 1, 2023",6:40:17 AM,6:41:08 AM,7:21:53 PM,7:21:02 PM +"Apr 2, 2023",6:38:40 AM,6:39:32 AM,7:22:54 PM,7:22:03 PM +"Apr 3, 2023",6:37:04 AM,6:37:56 AM,7:23:55 PM,7:23:04 PM +"Apr 4, 2023",6:35:28 AM,6:36:20 AM,7:24:57 PM,7:24:05 PM +"Apr 5, 2023",6:33:53 AM,6:34:44 AM,7:25:58 PM,7:25:06 PM +"Apr 6, 2023",6:32:17 AM,6:33:09 AM,7:26:59 PM,7:26:07 PM +"Apr 7, 2023",6:30:43 AM,6:31:34 AM,7:28:00 PM,7:27:08 PM +"Apr 8, 2023",6:29:08 AM,6:30:00 AM,7:29:01 PM,7:28:09 PM +"Apr 9, 2023",6:27:34 AM,6:28:26 AM,7:30:03 PM,7:29:11 PM +"Apr 10, 2023",6:26:01 AM,6:26:53 AM,7:31:04 PM,7:30:12 PM +"Apr 11, 2023",6:24:28 AM,6:25:20 AM,7:32:05 PM,7:31:13 PM +"Apr 12, 2023",6:22:55 AM,6:23:48 AM,7:33:06 PM,7:32:14 PM +"Apr 13, 2023",6:21:24 AM,6:22:16 AM,7:34:08 PM,7:33:15 PM +"Apr 14, 2023",6:19:52 AM,6:20:45 AM,7:35:09 PM,7:34:16 PM +"Apr 15, 2023",6:18:22 AM,6:19:14 AM,7:36:10 PM,7:35:18 PM +"Apr 16, 2023",6:16:52 AM,6:17:45 AM,7:37:12 PM,7:36:19 PM +"Apr 17, 2023",6:15:23 AM,6:16:15 AM,7:38:13 PM,7:37:20 PM +"Apr 18, 2023",6:13:54 AM,6:14:47 AM,7:39:15 PM,7:38:22 PM +"Apr 19, 2023",6:12:27 AM,6:13:20 AM,7:40:16 PM,7:39:23 PM +"Apr 20, 2023",6:11:00 AM,6:11:53 AM,7:41:17 PM,7:40:24 PM +"Apr 21, 2023",6:09:34 AM,6:10:27 AM,7:42:19 PM,7:41:26 PM +"Apr 22, 2023",6:08:09 AM,6:09:02 AM,7:43:20 PM,7:42:27 PM +"Apr 23, 2023",6:06:44 AM,6:07:38 AM,7:44:22 PM,7:43:28 PM +"Apr 24, 2023",6:05:21 AM,6:06:14 AM,7:45:23 PM,7:44:30 PM +"Apr 25, 2023",6:03:59 AM,6:04:52 AM,7:46:25 PM,7:45:31 PM +"Apr 26, 2023",6:02:37 AM,6:03:31 AM,7:47:26 PM,7:46:32 PM +"Apr 27, 2023",6:01:17 AM,6:02:11 AM,7:48:28 PM,7:47:33 PM +"Apr 28, 2023",5:59:57 AM,6:00:51 AM,7:49:29 PM,7:48:35 PM +"Apr 29, 2023",5:58:39 AM,5:59:33 AM,7:50:30 PM,7:49:36 PM +"Apr 30, 2023",5:57:22 AM,5:58:16 AM,7:51:31 PM,7:50:37 PM +"May 1, 2023",5:56:06 AM,5:57:00 AM,7:52:32 PM,7:51:38 PM +"May 2, 2023",5:54:51 AM,5:55:46 AM,7:53:33 PM,7:52:38 PM +"May 3, 2023",5:53:37 AM,5:54:32 AM,7:54:34 PM,7:53:39 PM +"May 4, 2023",5:52:25 AM,5:53:20 AM,7:55:35 PM,7:54:39 PM +"May 5, 2023",5:51:14 AM,5:52:09 AM,7:56:35 PM,7:55:40 PM +"May 6, 2023",5:50:04 AM,5:50:59 AM,7:57:35 PM,7:56:40 PM +"May 7, 2023",5:48:56 AM,5:49:51 AM,7:58:35 PM,7:57:40 PM +"May 8, 2023",5:47:48 AM,5:48:44 AM,7:59:35 PM,7:58:39 PM +"May 9, 2023",5:46:43 AM,5:47:38 AM,8:00:34 PM,7:59:39 PM +"May 10, 2023",5:45:38 AM,5:46:34 AM,8:01:34 PM,8:00:38 PM +"May 11, 2023",5:44:35 AM,5:45:31 AM,8:02:32 PM,8:01:36 PM +"May 12, 2023",5:43:34 AM,5:44:30 AM,8:03:31 PM,8:02:35 PM +"May 13, 2023",5:42:34 AM,5:43:31 AM,8:04:29 PM,8:03:33 PM +"May 14, 2023",5:41:36 AM,5:42:32 AM,8:05:27 PM,8:04:30 PM +"May 15, 2023",5:40:39 AM,5:41:36 AM,8:06:24 PM,8:05:27 PM +"May 16, 2023",5:39:44 AM,5:40:41 AM,8:07:21 PM,8:06:24 PM +"May 17, 2023",5:38:50 AM,5:39:47 AM,8:08:17 PM,8:07:20 PM +"May 18, 2023",5:37:58 AM,5:38:55 AM,8:09:13 PM,8:08:15 PM +"May 19, 2023",5:37:08 AM,5:38:05 AM,8:10:08 PM,8:09:10 PM +"May 20, 2023",5:36:20 AM,5:37:17 AM,8:11:02 PM,8:10:05 PM +"May 21, 2023",5:35:33 AM,5:36:30 AM,8:11:56 PM,8:10:58 PM +"May 22, 2023",5:34:48 AM,5:35:45 AM,8:12:49 PM,8:11:51 PM +"May 23, 2023",5:34:04 AM,5:35:02 AM,8:13:41 PM,8:12:43 PM +"May 24, 2023",5:33:23 AM,5:34:21 AM,8:14:33 PM,8:13:35 PM +"May 25, 2023",5:32:43 AM,5:33:41 AM,8:15:24 PM,8:14:25 PM +"May 26, 2023",5:32:05 AM,5:33:03 AM,8:16:13 PM,8:15:15 PM +"May 27, 2023",5:31:29 AM,5:32:27 AM,8:17:02 PM,8:16:04 PM +"May 28, 2023",5:30:55 AM,5:31:53 AM,8:17:50 PM,8:16:52 PM +"May 29, 2023",5:30:23 AM,5:31:21 AM,8:18:37 PM,8:17:39 PM +"May 30, 2023",5:29:52 AM,5:30:51 AM,8:19:23 PM,8:18:24 PM +"May 31, 2023",5:29:24 AM,5:30:22 AM,8:20:08 PM,8:19:09 PM +"Jun 1, 2023",5:28:57 AM,5:29:56 AM,8:20:52 PM,8:19:53 PM +"Jun 2, 2023",5:28:32 AM,5:29:31 AM,8:21:35 PM,8:20:35 PM +"Jun 3, 2023",5:28:09 AM,5:29:09 AM,8:22:16 PM,8:21:17 PM +"Jun 4, 2023",5:27:49 AM,5:28:48 AM,8:22:56 PM,8:21:57 PM +"Jun 5, 2023",5:27:30 AM,5:28:29 AM,8:23:35 PM,8:22:36 PM +"Jun 6, 2023",5:27:13 AM,5:28:12 AM,8:24:13 PM,8:23:13 PM +"Jun 7, 2023",5:26:58 AM,5:27:57 AM,8:24:49 PM,8:23:49 PM +"Jun 8, 2023",5:26:45 AM,5:27:45 AM,8:25:24 PM,8:24:24 PM +"Jun 9, 2023",5:26:34 AM,5:27:34 AM,8:25:57 PM,8:24:57 PM +"Jun 10, 2023",5:26:25 AM,5:27:25 AM,8:26:29 PM,8:25:29 PM +"Jun 11, 2023",5:26:18 AM,5:27:17 AM,8:26:59 PM,8:25:59 PM +"Jun 12, 2023",5:26:12 AM,5:27:12 AM,8:27:28 PM,8:26:28 PM +"Jun 13, 2023",5:26:09 AM,5:27:09 AM,8:27:55 PM,8:26:55 PM +"Jun 14, 2023",5:26:08 AM,5:27:08 AM,8:28:20 PM,8:27:20 PM +"Jun 15, 2023",5:26:08 AM,5:27:08 AM,8:28:44 PM,8:27:44 PM +"Jun 16, 2023",5:26:11 AM,5:27:11 AM,8:29:06 PM,8:28:06 PM +"Jun 17, 2023",5:26:15 AM,5:27:15 AM,8:29:27 PM,8:28:27 PM +"Jun 18, 2023",5:26:21 AM,5:27:22 AM,8:29:46 PM,8:28:45 PM +"Jun 19, 2023",5:26:29 AM,5:27:30 AM,8:30:02 PM,8:29:02 PM +"Jun 20, 2023",5:26:39 AM,5:27:40 AM,8:30:17 PM,8:29:17 PM +"Jun 21, 2023",5:26:51 AM,5:27:51 AM,8:30:31 PM,8:29:30 PM +"Jun 22, 2023",5:27:05 AM,5:28:05 AM,8:30:42 PM,8:29:42 PM +"Jun 23, 2023",5:27:20 AM,5:28:20 AM,8:30:52 PM,8:29:51 PM +"Jun 24, 2023",5:27:37 AM,5:28:37 AM,8:30:59 PM,8:29:59 PM +"Jun 25, 2023",5:27:56 AM,5:28:56 AM,8:31:05 PM,8:30:05 PM +"Jun 26, 2023",5:28:16 AM,5:29:16 AM,8:31:09 PM,8:30:09 PM +"Jun 27, 2023",5:28:38 AM,5:29:38 AM,8:31:11 PM,8:30:10 PM +"Jun 28, 2023",5:29:02 AM,5:30:02 AM,8:31:10 PM,8:30:10 PM +"Jun 29, 2023",5:29:27 AM,5:30:27 AM,8:31:08 PM,8:30:08 PM +"Jun 30, 2023",5:29:54 AM,5:30:54 AM,8:31:04 PM,8:30:04 PM +"Jul 1, 2023",5:30:22 AM,5:31:22 AM,8:30:58 PM,8:29:58 PM +"Jul 2, 2023",5:30:52 AM,5:31:52 AM,8:30:50 PM,8:29:50 PM +"Jul 3, 2023",5:31:23 AM,5:32:23 AM,8:30:40 PM,8:29:40 PM +"Jul 4, 2023",5:31:56 AM,5:32:56 AM,8:30:28 PM,8:29:28 PM +"Jul 5, 2023",5:32:30 AM,5:33:30 AM,8:30:14 PM,8:29:14 PM +"Jul 6, 2023",5:33:06 AM,5:34:05 AM,8:29:58 PM,8:28:58 PM +"Jul 7, 2023",5:33:42 AM,5:34:42 AM,8:29:40 PM,8:28:40 PM +"Jul 8, 2023",5:34:20 AM,5:35:20 AM,8:29:20 PM,8:28:20 PM +"Jul 9, 2023",5:34:59 AM,5:35:59 AM,8:28:58 PM,8:27:58 PM +"Jul 10, 2023",5:35:39 AM,5:36:39 AM,8:28:34 PM,8:27:35 PM +"Jul 11, 2023",5:36:21 AM,5:37:20 AM,8:28:08 PM,8:27:09 PM +"Jul 12, 2023",5:37:03 AM,5:38:02 AM,8:27:40 PM,8:26:41 PM +"Jul 13, 2023",5:37:47 AM,5:38:46 AM,8:27:10 PM,8:26:11 PM +"Jul 14, 2023",5:38:31 AM,5:39:30 AM,8:26:38 PM,8:25:40 PM +"Jul 15, 2023",5:39:17 AM,5:40:15 AM,8:26:04 PM,8:25:06 PM +"Jul 16, 2023",5:40:03 AM,5:41:02 AM,8:25:29 PM,8:24:30 PM +"Jul 17, 2023",5:40:50 AM,5:41:49 AM,8:24:51 PM,8:23:53 PM +"Jul 18, 2023",5:41:38 AM,5:42:36 AM,8:24:12 PM,8:23:14 PM +"Jul 19, 2023",5:42:27 AM,5:43:25 AM,8:23:31 PM,8:22:33 PM +"Jul 20, 2023",5:43:17 AM,5:44:15 AM,8:22:48 PM,8:21:50 PM +"Jul 21, 2023",5:44:07 AM,5:45:05 AM,8:22:03 PM,8:21:05 PM +"Jul 22, 2023",5:44:58 AM,5:45:55 AM,8:21:16 PM,8:20:19 PM +"Jul 23, 2023",5:45:49 AM,5:46:47 AM,8:20:28 PM,8:19:30 PM +"Jul 24, 2023",5:46:42 AM,5:47:39 AM,8:19:38 PM,8:18:41 PM +"Jul 25, 2023",5:47:34 AM,5:48:31 AM,8:18:46 PM,8:17:49 PM +"Jul 26, 2023",5:48:27 AM,5:49:25 AM,8:17:52 PM,8:16:56 PM +"Jul 27, 2023",5:49:21 AM,5:50:18 AM,8:16:57 PM,8:16:00 PM +"Jul 28, 2023",5:50:15 AM,5:51:12 AM,8:16:00 PM,8:15:04 PM +"Jul 29, 2023",5:51:10 AM,5:52:06 AM,8:15:02 PM,8:14:06 PM +"Jul 30, 2023",5:52:05 AM,5:53:01 AM,8:14:02 PM,8:13:06 PM +"Jul 31, 2023",5:53:00 AM,5:53:56 AM,8:13:01 PM,8:12:04 PM +"Aug 1, 2023",5:53:56 AM,5:54:52 AM,8:11:58 PM,8:11:02 PM +"Aug 2, 2023",5:54:52 AM,5:55:48 AM,8:10:53 PM,8:09:57 PM +"Aug 3, 2023",5:55:48 AM,5:56:44 AM,8:09:47 PM,8:08:51 PM +"Aug 4, 2023",5:56:44 AM,5:57:40 AM,8:08:40 PM,8:07:44 PM +"Aug 5, 2023",5:57:41 AM,5:58:36 AM,8:07:31 PM,8:06:35 PM +"Aug 6, 2023",5:58:38 AM,5:59:33 AM,8:06:21 PM,8:05:25 PM +"Aug 7, 2023",5:59:35 AM,6:00:30 AM,8:05:09 PM,8:04:14 PM +"Aug 8, 2023",6:00:32 AM,6:01:27 AM,8:03:56 PM,8:03:01 PM +"Aug 9, 2023",6:01:29 AM,6:02:24 AM,8:02:42 PM,8:01:47 PM +"Aug 10, 2023",6:02:26 AM,6:03:21 AM,8:01:27 PM,8:00:32 PM +"Aug 11, 2023",6:03:24 AM,6:04:18 AM,8:00:10 PM,7:59:16 PM +"Aug 12, 2023",6:04:21 AM,6:05:16 AM,7:58:53 PM,7:57:58 PM +"Aug 13, 2023",6:05:19 AM,6:06:13 AM,7:57:34 PM,7:56:39 PM +"Aug 14, 2023",6:06:16 AM,6:07:11 AM,7:56:14 PM,7:55:20 PM +"Aug 15, 2023",6:07:14 AM,6:08:08 AM,7:54:53 PM,7:53:59 PM +"Aug 16, 2023",6:08:12 AM,6:09:06 AM,7:53:30 PM,7:52:37 PM +"Aug 17, 2023",6:09:09 AM,6:10:03 AM,7:52:07 PM,7:51:14 PM +"Aug 18, 2023",6:10:07 AM,6:11:01 AM,7:50:43 PM,7:49:50 PM +"Aug 19, 2023",6:11:05 AM,6:11:58 AM,7:49:18 PM,7:48:25 PM +"Aug 20, 2023",6:12:02 AM,6:12:56 AM,7:47:52 PM,7:46:59 PM +"Aug 21, 2023",6:13:00 AM,6:13:53 AM,7:46:25 PM,7:45:32 PM +"Aug 22, 2023",6:13:57 AM,6:14:51 AM,7:44:57 PM,7:44:04 PM +"Aug 23, 2023",6:14:55 AM,6:15:48 AM,7:43:29 PM,7:42:36 PM +"Aug 24, 2023",6:15:52 AM,6:16:45 AM,7:41:59 PM,7:41:06 PM +"Aug 25, 2023",6:16:50 AM,6:17:43 AM,7:40:29 PM,7:39:36 PM +"Aug 26, 2023",6:17:47 AM,6:18:40 AM,7:38:58 PM,7:38:06 PM +"Aug 27, 2023",6:18:44 AM,6:19:37 AM,7:37:26 PM,7:36:34 PM +"Aug 28, 2023",6:19:42 AM,6:20:34 AM,7:35:54 PM,7:35:02 PM +"Aug 29, 2023",6:20:39 AM,6:21:31 AM,7:34:21 PM,7:33:29 PM +"Aug 30, 2023",6:21:36 AM,6:22:28 AM,7:32:47 PM,7:31:55 PM +"Aug 31, 2023",6:22:33 AM,6:23:25 AM,7:31:13 PM,7:30:21 PM +"Sep 1, 2023",6:23:30 AM,6:24:22 AM,7:29:39 PM,7:28:47 PM +"Sep 2, 2023",6:24:27 AM,6:25:19 AM,7:28:03 PM,7:27:11 PM +"Sep 3, 2023",6:25:24 AM,6:26:16 AM,7:26:28 PM,7:25:36 PM +"Sep 4, 2023",6:26:21 AM,6:27:13 AM,7:24:51 PM,7:24:00 PM +"Sep 5, 2023",6:27:18 AM,6:28:09 AM,7:23:15 PM,7:22:23 PM +"Sep 6, 2023",6:28:14 AM,6:29:06 AM,7:21:37 PM,7:20:46 PM +"Sep 7, 2023",6:29:11 AM,6:30:03 AM,7:20:00 PM,7:19:08 PM +"Sep 8, 2023",6:30:08 AM,6:31:00 AM,7:18:22 PM,7:17:31 PM +"Sep 9, 2023",6:31:05 AM,6:31:56 AM,7:16:44 PM,7:15:53 PM +"Sep 10, 2023",6:32:02 AM,6:32:53 AM,7:15:06 PM,7:14:14 PM +"Sep 11, 2023",6:32:58 AM,6:33:50 AM,7:13:27 PM,7:12:36 PM +"Sep 12, 2023",6:33:55 AM,6:34:46 AM,7:11:48 PM,7:10:57 PM +"Sep 13, 2023",6:34:52 AM,6:35:43 AM,7:10:09 PM,7:09:18 PM +"Sep 14, 2023",6:35:49 AM,6:36:40 AM,7:08:29 PM,7:07:38 PM +"Sep 15, 2023",6:36:46 AM,6:37:37 AM,7:06:50 PM,7:05:59 PM +"Sep 16, 2023",6:37:42 AM,6:38:34 AM,7:05:10 PM,7:04:19 PM +"Sep 17, 2023",6:38:39 AM,6:39:31 AM,7:03:31 PM,7:02:39 PM +"Sep 18, 2023",6:39:36 AM,6:40:28 AM,7:01:51 PM,7:01:00 PM +"Sep 19, 2023",6:40:34 AM,6:41:25 AM,7:00:11 PM,6:59:20 PM +"Sep 20, 2023",6:41:31 AM,6:42:22 AM,6:58:31 PM,6:57:40 PM +"Sep 21, 2023",6:42:28 AM,6:43:19 AM,6:56:51 PM,6:56:00 PM +"Sep 22, 2023",6:43:26 AM,6:44:17 AM,6:55:12 PM,6:54:21 PM +"Sep 23, 2023",6:44:23 AM,6:45:14 AM,6:53:32 PM,6:52:41 PM +"Sep 24, 2023",6:45:21 AM,6:46:12 AM,6:51:52 PM,6:51:01 PM +"Sep 25, 2023",6:46:19 AM,6:47:10 AM,6:50:13 PM,6:49:22 PM +"Sep 26, 2023",6:47:17 AM,6:48:08 AM,6:48:34 PM,6:47:43 PM +"Sep 27, 2023",6:48:15 AM,6:49:06 AM,6:46:55 PM,6:46:04 PM +"Sep 28, 2023",6:49:13 AM,6:50:04 AM,6:45:16 PM,6:44:25 PM +"Sep 29, 2023",6:50:11 AM,6:51:03 AM,6:43:37 PM,6:42:46 PM +"Sep 30, 2023",6:51:10 AM,6:52:01 AM,6:41:59 PM,6:41:08 PM +"Oct 1, 2023",6:52:09 AM,6:53:00 AM,6:40:21 PM,6:39:30 PM +"Oct 2, 2023",6:53:08 AM,6:53:59 AM,6:38:44 PM,6:37:53 PM +"Oct 3, 2023",6:54:08 AM,6:54:59 AM,6:37:06 PM,6:36:15 PM +"Oct 4, 2023",6:55:07 AM,6:55:58 AM,6:35:30 PM,6:34:39 PM +"Oct 5, 2023",6:56:07 AM,6:56:58 AM,6:33:53 PM,6:33:02 PM +"Oct 6, 2023",6:57:07 AM,6:57:58 AM,6:32:17 PM,6:31:26 PM +"Oct 7, 2023",6:58:07 AM,6:58:59 AM,6:30:42 PM,6:29:51 PM +"Oct 8, 2023",6:59:08 AM,7:00:00 AM,6:29:07 PM,6:28:16 PM +"Oct 9, 2023",7:00:09 AM,7:01:01 AM,6:27:33 PM,6:26:42 PM +"Oct 10, 2023",7:01:10 AM,7:02:02 AM,6:25:59 PM,6:25:08 PM +"Oct 11, 2023",7:02:12 AM,7:03:03 AM,6:24:26 PM,6:23:35 PM +"Oct 12, 2023",7:03:14 AM,7:04:05 AM,6:22:54 PM,6:22:03 PM +"Oct 13, 2023",7:04:16 AM,7:05:07 AM,6:21:23 PM,6:20:31 PM +"Oct 14, 2023",7:05:18 AM,7:06:10 AM,6:19:52 PM,6:19:00 PM +"Oct 15, 2023",7:06:21 AM,7:07:13 AM,6:18:22 PM,6:17:30 PM +"Oct 16, 2023",7:07:24 AM,7:08:16 AM,6:16:52 PM,6:16:00 PM +"Oct 17, 2023",7:08:28 AM,7:09:20 AM,6:15:24 PM,6:14:32 PM +"Oct 18, 2023",7:09:31 AM,7:10:23 AM,6:13:56 PM,6:13:04 PM +"Oct 19, 2023",7:10:35 AM,7:11:28 AM,6:12:30 PM,6:11:37 PM +"Oct 20, 2023",7:11:40 AM,7:12:32 AM,6:11:04 PM,6:10:12 PM +"Oct 21, 2023",7:12:45 AM,7:13:37 AM,6:09:39 PM,6:08:47 PM +"Oct 22, 2023",7:13:50 AM,7:14:42 AM,6:08:15 PM,6:07:23 PM +"Oct 23, 2023",7:14:55 AM,7:15:48 AM,6:06:53 PM,6:06:00 PM +"Oct 24, 2023",7:16:01 AM,7:16:54 AM,6:05:31 PM,6:04:38 PM +"Oct 25, 2023",7:17:07 AM,7:18:00 AM,6:04:11 PM,6:03:18 PM +"Oct 26, 2023",7:18:13 AM,7:19:06 AM,6:02:51 PM,6:01:58 PM +"Oct 27, 2023",7:19:20 AM,7:20:13 AM,6:01:33 PM,6:00:40 PM +"Oct 28, 2023",7:20:27 AM,7:21:20 AM,6:00:16 PM,5:59:23 PM +"Oct 29, 2023",7:21:34 AM,7:22:27 AM,5:59:01 PM,5:58:07 PM +"Oct 30, 2023",7:22:42 AM,7:23:35 AM,5:57:46 PM,5:56:53 PM +"Oct 31, 2023",7:23:49 AM,7:24:43 AM,5:56:33 PM,5:55:40 PM +"Nov 1, 2023",7:24:58 AM,7:25:51 AM,5:55:22 PM,5:54:28 PM +"Nov 2, 2023",7:26:06 AM,7:27:00 AM,5:54:12 PM,5:53:18 PM +"Nov 3, 2023",7:27:14 AM,7:28:08 AM,5:53:03 PM,5:52:09 PM +"Nov 4, 2023",7:28:23 AM,7:29:17 AM,5:51:55 PM,5:51:01 PM +"Nov 5, 2023",6:29:32 AM,6:30:26 AM,4:50:50 PM,4:49:55 PM +"Nov 6, 2023",6:30:41 AM,6:31:35 AM,4:49:45 PM,4:48:51 PM +"Nov 7, 2023",6:31:50 AM,6:32:44 AM,4:48:43 PM,4:47:48 PM +"Nov 8, 2023",6:32:59 AM,6:33:54 AM,4:47:42 PM,4:46:47 PM +"Nov 9, 2023",6:34:08 AM,6:35:03 AM,4:46:42 PM,4:45:48 PM +"Nov 10, 2023",6:35:18 AM,6:36:13 AM,4:45:45 PM,4:44:50 PM +"Nov 11, 2023",6:36:27 AM,6:37:22 AM,4:44:49 PM,4:43:54 PM +"Nov 12, 2023",6:37:36 AM,6:38:32 AM,4:43:54 PM,4:42:59 PM +"Nov 13, 2023",6:38:46 AM,6:39:41 AM,4:43:02 PM,4:42:07 PM +"Nov 14, 2023",6:39:55 AM,6:40:50 AM,4:42:11 PM,4:41:16 PM +"Nov 15, 2023",6:41:04 AM,6:42:00 AM,4:41:22 PM,4:40:27 PM +"Nov 16, 2023",6:42:13 AM,6:43:09 AM,4:40:36 PM,4:39:40 PM +"Nov 17, 2023",6:43:22 AM,6:44:18 AM,4:39:51 PM,4:38:55 PM +"Nov 18, 2023",6:44:30 AM,6:45:26 AM,4:39:07 PM,4:38:11 PM +"Nov 19, 2023",6:45:38 AM,6:46:34 AM,4:38:26 PM,4:37:30 PM +"Nov 20, 2023",6:46:46 AM,6:47:42 AM,4:37:47 PM,4:36:51 PM +"Nov 21, 2023",6:47:53 AM,6:48:50 AM,4:37:10 PM,4:36:14 PM +"Nov 22, 2023",6:49:00 AM,6:49:57 AM,4:36:35 PM,4:35:38 PM +"Nov 23, 2023",6:50:07 AM,6:51:04 AM,4:36:02 PM,4:35:05 PM +"Nov 24, 2023",6:51:13 AM,6:52:10 AM,4:35:31 PM,4:34:34 PM +"Nov 25, 2023",6:52:19 AM,6:53:16 AM,4:35:02 PM,4:34:05 PM +"Nov 26, 2023",6:53:23 AM,6:54:21 AM,4:34:36 PM,4:33:38 PM +"Nov 27, 2023",6:54:28 AM,6:55:25 AM,4:34:11 PM,4:33:14 PM +"Nov 28, 2023",6:55:31 AM,6:56:28 AM,4:33:49 PM,4:32:51 PM +"Nov 29, 2023",6:56:34 AM,6:57:31 AM,4:33:29 PM,4:32:31 PM +"Nov 30, 2023",6:57:35 AM,6:58:33 AM,4:33:11 PM,4:32:13 PM +"Dec 1, 2023",6:58:36 AM,6:59:34 AM,4:32:55 PM,4:31:57 PM +"Dec 2, 2023",6:59:36 AM,7:00:34 AM,4:32:41 PM,4:31:43 PM +"Dec 3, 2023",7:00:35 AM,7:01:33 AM,4:32:30 PM,4:31:32 PM +"Dec 4, 2023",7:01:33 AM,7:02:31 AM,4:32:21 PM,4:31:23 PM +"Dec 5, 2023",7:02:30 AM,7:03:28 AM,4:32:14 PM,4:31:16 PM +"Dec 6, 2023",7:03:26 AM,7:04:24 AM,4:32:09 PM,4:31:11 PM +"Dec 7, 2023",7:04:20 AM,7:05:18 AM,4:32:07 PM,4:31:09 PM +"Dec 8, 2023",7:05:13 AM,7:06:12 AM,4:32:07 PM,4:31:09 PM +"Dec 9, 2023",7:06:05 AM,7:07:03 AM,4:32:09 PM,4:31:11 PM +"Dec 10, 2023",7:06:55 AM,7:07:54 AM,4:32:14 PM,4:31:15 PM +"Dec 11, 2023",7:07:44 AM,7:08:43 AM,4:32:20 PM,4:31:22 PM +"Dec 12, 2023",7:08:32 AM,7:09:30 AM,4:32:29 PM,4:31:30 PM +"Dec 13, 2023",7:09:18 AM,7:10:16 AM,4:32:40 PM,4:31:42 PM +"Dec 14, 2023",7:10:02 AM,7:11:01 AM,4:32:54 PM,4:31:55 PM +"Dec 15, 2023",7:10:45 AM,7:11:44 AM,4:33:09 PM,4:32:10 PM +"Dec 16, 2023",7:11:26 AM,7:12:25 AM,4:33:27 PM,4:32:28 PM +"Dec 17, 2023",7:12:05 AM,7:13:04 AM,4:33:47 PM,4:32:48 PM +"Dec 18, 2023",7:12:42 AM,7:13:41 AM,4:34:09 PM,4:33:10 PM +"Dec 19, 2023",7:13:18 AM,7:14:17 AM,4:34:33 PM,4:33:34 PM +"Dec 20, 2023",7:13:52 AM,7:14:51 AM,4:34:59 PM,4:34:00 PM +"Dec 21, 2023",7:14:24 AM,7:15:23 AM,4:35:27 PM,4:34:28 PM +"Dec 22, 2023",7:14:54 AM,7:15:53 AM,4:35:58 PM,4:34:59 PM +"Dec 23, 2023",7:15:22 AM,7:16:21 AM,4:36:30 PM,4:35:31 PM +"Dec 24, 2023",7:15:48 AM,7:16:47 AM,4:37:04 PM,4:36:05 PM +"Dec 25, 2023",7:16:12 AM,7:17:11 AM,4:37:40 PM,4:36:41 PM +"Dec 26, 2023",7:16:34 AM,7:17:33 AM,4:38:18 PM,4:37:19 PM +"Dec 27, 2023",7:16:54 AM,7:17:53 AM,4:38:58 PM,4:37:59 PM +"Dec 28, 2023",7:17:12 AM,7:18:11 AM,4:39:40 PM,4:38:41 PM +"Dec 29, 2023",7:17:27 AM,7:18:26 AM,4:40:23 PM,4:39:24 PM +"Dec 30, 2023",7:17:41 AM,7:18:40 AM,4:41:08 PM,4:40:09 PM +"Dec 31, 2023",7:17:52 AM,7:18:51 AM,4:41:55 PM,4:40:56 PM From c119b95a1259c935b19f8bc0feb865584e3dfae5 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Fri, 29 Dec 2023 14:35:46 -0500 Subject: [PATCH 07/15] Add code to read tests from file. --- KosherCocoaTests/KCNOAACalculatorTests.swift | 41 ++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/KosherCocoaTests/KCNOAACalculatorTests.swift b/KosherCocoaTests/KCNOAACalculatorTests.swift index 39e4d0b..33bfa25 100644 --- a/KosherCocoaTests/KCNOAACalculatorTests.swift +++ b/KosherCocoaTests/KCNOAACalculatorTests.swift @@ -27,6 +27,47 @@ final class KCNOAACalculatorTests: XCTestCase { AstronomicalCalendar(location: lakewoodCalculator.geoLocation) }() + + + /// Splits the file into a multidimensional array. + /// - Parameter file: The name of a CSV file in the test bundle. + /// - Returns: A multidimensional array of test cases. + /// + /// The first row contains the names of the methods to be tested. + /// Each of the following rows represents a test case. + /// - The first item is the Gregorian date. + /// - Each of the following values is the expected output for the method named in that column header. + @available(iOS 16.0, *) + private class func testCases(from file:String) throws -> [[String]] { + guard let url = Bundle(for: self).url(forResource: file, withExtension: "csv") + else { + return [] + } + + let rawTestCases = try String(contentsOf: url, encoding: .utf8) + let lines: [String] = rawTestCases.components(separatedBy:.newlines).filter{ !$0.isEmpty } + let commaCharacterSet: CharacterSet = CharacterSet(charactersIn: ",") + + let headers: [[String]] = [lines[0] + .components(separatedBy:commaCharacterSet) + .compactMap {$0.trimmingCharacters(in: .whitespacesAndNewlines)} + ] + let testCases: [[String]] = lines[1...] + .compactMap { + $0 + .replacing(",", with: "", maxReplacements: 1) + .components(separatedBy:commaCharacterSet) + .compactMap { + NSString(string: $0) + .replacingOccurrences(of:"", with: String(",")) + .replacingOccurrences(of: "\"", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + } + } + return headers + testCases + } + + override func setUpWithError() throws { try super.setUpWithError() } From 676a0bc448ff795d780ecd56b8e3b4ddbf6510d6 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Sun, 31 Dec 2023 01:56:42 -0500 Subject: [PATCH 08/15] Add code to 1) load test cases from CSV files. 2) Generate tests inside the Obj-C runtime. 3) A bridging header for Swift-ObjC interop. --- KosherCocoa.xcodeproj/project.pbxproj | 14 +++- KosherCocoaTests/KCDynamicTestCase.m | 73 +++++++++++++++++++ .../KosherCocoaTests-Bridging-Header.h | 4 + KosherCocoaTests/TestLoader.swift | 50 +++++++++++++ 4 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 KosherCocoaTests/KCDynamicTestCase.m create mode 100644 KosherCocoaTests/KosherCocoaTests-Bridging-Header.h create mode 100644 KosherCocoaTests/TestLoader.swift diff --git a/KosherCocoa.xcodeproj/project.pbxproj b/KosherCocoa.xcodeproj/project.pbxproj index 22dddf0..1b32c9a 100644 --- a/KosherCocoa.xcodeproj/project.pbxproj +++ b/KosherCocoa.xcodeproj/project.pbxproj @@ -83,6 +83,8 @@ F52587431ECD019A009E623C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F52587411ECD019A009E623C /* Localizable.strings */; }; F5C6076F2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */; }; F5C607722B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv in Resources */ = {isa = PBXBuildFile; fileRef = F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */; }; + F5C607772B3F5A4E00A1E94C /* KCDynamicTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C607762B3F5A4E00A1E94C /* KCDynamicTestCase.m */; }; + F5C607792B3F5BE600A1E94C /* TestLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C607782B3F5BE600A1E94C /* TestLoader.swift */; }; F5F902FA1FC53B17003FE90D /* KosherCocoaMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */; }; F5F902FC1FC53C01003FE90D /* KCJewishCalendarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */; }; /* End PBXBuildFile section */ @@ -184,6 +186,9 @@ F52587421ECD019A009E623C /* en */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = en; path = Localizations/en.lproj/Localizable.strings; sourceTree = ""; }; F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCAstronomicalCalculatorTests.swift; sourceTree = ""; }; F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SunriseSunsetLakewoodNOAA.csv; sourceTree = ""; }; + F5C607752B3F5A4D00A1E94C /* KosherCocoaTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "KosherCocoaTests-Bridging-Header.h"; sourceTree = ""; }; + F5C607762B3F5A4E00A1E94C /* KCDynamicTestCase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KCDynamicTestCase.m; sourceTree = ""; }; + F5C607782B3F5BE600A1E94C /* TestLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLoader.swift; sourceTree = ""; }; F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCJewishCalendarTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -460,11 +465,14 @@ children = ( F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */, 5D435CE92A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift */, + F5C607782B3F5BE600A1E94C /* TestLoader.swift */, + F5C607762B3F5A4E00A1E94C /* KCDynamicTestCase.m */, F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */, 46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */, F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */, 5D435CF32A3C389B00F66AE0 /* Screenshot 2023-06-16 at 2.27.26 AM.png */, 46DB495C1D9837D100F3A576 /* Info.plist */, + F5C607752B3F5A4D00A1E94C /* KosherCocoaTests-Bridging-Header.h */, ); path = KosherCocoaTests; sourceTree = ""; @@ -577,7 +585,7 @@ }; 46DB49631D9839F100F3A576 = { CreatedOnToolsVersion = 8.0; - LastSwiftMigration = ""; + LastSwiftMigration = 1510; ProvisioningStyle = Automatic; }; }; @@ -667,6 +675,8 @@ F5C6076F2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift in Sources */, F5F902FA1FC53B17003FE90D /* KosherCocoaMetadataTests.swift in Sources */, F5F902FC1FC53C01003FE90D /* KCJewishCalendarTests.swift in Sources */, + F5C607792B3F5BE600A1E94C /* TestLoader.swift in Sources */, + F5C607772B3F5A4E00A1E94C /* KCDynamicTestCase.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -905,6 +915,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = "KosherCocoaTests/KosherCocoaTests-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; @@ -936,6 +947,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.mosheberman.KosherCocoaTests; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; + SWIFT_OBJC_BRIDGING_HEADER = "KosherCocoaTests/KosherCocoaTests-Bridging-Header.h"; SWIFT_SWIFT3_OBJC_INFERENCE = Default; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2,3,4"; diff --git a/KosherCocoaTests/KCDynamicTestCase.m b/KosherCocoaTests/KCDynamicTestCase.m new file mode 100644 index 0000000..4f12e50 --- /dev/null +++ b/KosherCocoaTests/KCDynamicTestCase.m @@ -0,0 +1,73 @@ +// +// KCDynamicTestCase.m +// KosherCocoaTests +// +// Created by Moshe Berman on 12/29/23. +// Copyright © 2023 Moshe Berman. All rights reserved. +// + +#import +#import +#import +#import "KosherCocoaTests-Swift.h" + +typedef void(^TestCaseBlock)(void); + +@interface KCDynamicTestCase : XCTestCase + +@end + +@implementation KCDynamicTestCase + ++ (void)setUp +{ + +} + + ++ (void)addTestNamed:(NSString *)name withBlock:(TestCaseBlock)block { + SEL selForTestCase = NSSelectorFromString(name); + IMP implementation = imp_implementationWithBlock(^(id tc) { + block(); + }); + + class_addMethod(self, selForTestCase, implementation, "v:@"); +} + ++ (NSArray *)testInvocations { + NSMutableArray *invocations = [NSMutableArray array]; + + NSError *err = nil; + NSArray *> *tests = [TestsFromCSV loadTestsFrom:@"SunriseSunsetLakewoodNOAA" error:&err]; + if (!tests || err != nil) { + XCTFail(@"Could not read tests from CSV. %@", err.localizedDescription); + } + NSArray *methods = tests[0]; + + for (NSInteger testIdx = 1; testIdx > 0 && testIdx < tests.count; testIdx++) { + NSArray *testCase = tests[testIdx]; + + for (NSInteger methodIdx = 1; methodIdx < methods.count; methodIdx++) { + NSString *methodName = methods[methodIdx]; + NSString *dateFormattedForSelector = [[testCase.firstObject stringByReplacingOccurrencesOfString:@"," withString:@""] stringByReplacingOccurrencesOfString:@" " withString:@"_"]; + NSString *selectorString = [NSString stringWithFormat:@"test%@%@", [methodName capitalizedString] , dateFormattedForSelector]; + + [self addTestNamed:selectorString withBlock:^{ + // TODO: Call KosherCocoa in here! + }]; + + SEL selector = NSSelectorFromString(selectorString); + NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + invocation.selector = selector; + + [invocations addObject:invocation]; + + } + } + + return invocations; +} + + +@end diff --git a/KosherCocoaTests/KosherCocoaTests-Bridging-Header.h b/KosherCocoaTests/KosherCocoaTests-Bridging-Header.h new file mode 100644 index 0000000..1b2cb5d --- /dev/null +++ b/KosherCocoaTests/KosherCocoaTests-Bridging-Header.h @@ -0,0 +1,4 @@ +// +// Use this file to import your target's public headers that you would like to expose to Swift. +// + diff --git a/KosherCocoaTests/TestLoader.swift b/KosherCocoaTests/TestLoader.swift new file mode 100644 index 0000000..8fce679 --- /dev/null +++ b/KosherCocoaTests/TestLoader.swift @@ -0,0 +1,50 @@ +// +// TestLoader.swift +// KosherCocoaTests +// +// Created by Moshe Berman on 12/29/23. +// Copyright © 2023 Moshe Berman. All rights reserved. +// + +import Foundation + +/// Loads tests from CSV files. +@objc class TestsFromCSV: NSObject { + /// Splits the file into a multidimensional array. + /// - Parameter file: The name of a CSV file in the test bundle. + /// - Returns: A multidimensional array of test cases. + /// + /// The first row contains the names of the methods to be tested. + /// Each of the following rows represents a test case. + /// - The first item is the Gregorian date. + /// - Each of the following values is the expected output for the method named in that column header. + @objc public class func loadTests(from file:String) throws -> [[String]] { + guard let url = Bundle(for: self).url(forResource: file, withExtension: "csv") + else { + return [] + } + + let rawTestCases = try String(contentsOf: url, encoding: .utf8) + let lines: [String] = rawTestCases.components(separatedBy:.newlines).filter{ !$0.isEmpty } + let commaCharacterSet: CharacterSet = CharacterSet(charactersIn: ",") + + let headers: [[String]] = [lines[0] + .components(separatedBy:commaCharacterSet) + .compactMap {$0.trimmingCharacters(in: .whitespacesAndNewlines)} + ] + let testCases: [[String]] = lines[1...] + .compactMap { + let range = NSString(string: $0).range(of: ",") + return NSString(string: $0) + .replacingOccurrences(of: ",", with: "", range: range) + .components(separatedBy:commaCharacterSet) + .compactMap { + NSString(string: $0) + .replacingOccurrences(of:"", with: String(",")) + .replacingOccurrences(of: "\"", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + } + } + return headers + testCases + } +} From a463b4ce826c7ce9f2f59b1f80bb88ac0a6739ea Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Sun, 31 Dec 2023 01:57:06 -0500 Subject: [PATCH 09/15] Delete code that was moved into CSV loader. --- KosherCocoaTests/KCNOAACalculatorTests.swift | 41 -------------------- 1 file changed, 41 deletions(-) diff --git a/KosherCocoaTests/KCNOAACalculatorTests.swift b/KosherCocoaTests/KCNOAACalculatorTests.swift index 33bfa25..39e4d0b 100644 --- a/KosherCocoaTests/KCNOAACalculatorTests.swift +++ b/KosherCocoaTests/KCNOAACalculatorTests.swift @@ -27,47 +27,6 @@ final class KCNOAACalculatorTests: XCTestCase { AstronomicalCalendar(location: lakewoodCalculator.geoLocation) }() - - - /// Splits the file into a multidimensional array. - /// - Parameter file: The name of a CSV file in the test bundle. - /// - Returns: A multidimensional array of test cases. - /// - /// The first row contains the names of the methods to be tested. - /// Each of the following rows represents a test case. - /// - The first item is the Gregorian date. - /// - Each of the following values is the expected output for the method named in that column header. - @available(iOS 16.0, *) - private class func testCases(from file:String) throws -> [[String]] { - guard let url = Bundle(for: self).url(forResource: file, withExtension: "csv") - else { - return [] - } - - let rawTestCases = try String(contentsOf: url, encoding: .utf8) - let lines: [String] = rawTestCases.components(separatedBy:.newlines).filter{ !$0.isEmpty } - let commaCharacterSet: CharacterSet = CharacterSet(charactersIn: ",") - - let headers: [[String]] = [lines[0] - .components(separatedBy:commaCharacterSet) - .compactMap {$0.trimmingCharacters(in: .whitespacesAndNewlines)} - ] - let testCases: [[String]] = lines[1...] - .compactMap { - $0 - .replacing(",", with: "", maxReplacements: 1) - .components(separatedBy:commaCharacterSet) - .compactMap { - NSString(string: $0) - .replacingOccurrences(of:"", with: String(",")) - .replacingOccurrences(of: "\"", with: "") - .trimmingCharacters(in: .whitespacesAndNewlines) - } - } - return headers + testCases - } - - override func setUpWithError() throws { try super.setUpWithError() } From 72f8cbee4418e273431d67bfa2514baff36213b8 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Mon, 1 Jan 2024 21:49:28 -0500 Subject: [PATCH 10/15] Add dynamic tests. --- KosherCocoa.xcodeproj/project.pbxproj | 14 +- .../Library/Core/Solar/KCNOAACalculator.m | 2 +- .../DynamicNOAACalculatorTests.swift | 136 ++++ KosherCocoaTests/DynamicTestCase.h | 27 + KosherCocoaTests/DynamicTestCase.m | 71 ++ KosherCocoaTests/KCDynamicTestCase.m | 73 -- KosherCocoaTests/KCJewishCalendarTests.swift | 1 + .../KosherCocoaTests-Bridging-Header.h | 1 + .../SunriseSunsetLakewoodNOAA.csv | 730 +++++++++--------- 9 files changed, 612 insertions(+), 443 deletions(-) create mode 100644 KosherCocoaTests/DynamicNOAACalculatorTests.swift create mode 100644 KosherCocoaTests/DynamicTestCase.h create mode 100644 KosherCocoaTests/DynamicTestCase.m delete mode 100644 KosherCocoaTests/KCDynamicTestCase.m diff --git a/KosherCocoa.xcodeproj/project.pbxproj b/KosherCocoa.xcodeproj/project.pbxproj index 1b32c9a..f460367 100644 --- a/KosherCocoa.xcodeproj/project.pbxproj +++ b/KosherCocoa.xcodeproj/project.pbxproj @@ -83,8 +83,9 @@ F52587431ECD019A009E623C /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = F52587411ECD019A009E623C /* Localizable.strings */; }; F5C6076F2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */; }; F5C607722B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv in Resources */ = {isa = PBXBuildFile; fileRef = F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */; }; - F5C607772B3F5A4E00A1E94C /* KCDynamicTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C607762B3F5A4E00A1E94C /* KCDynamicTestCase.m */; }; + F5C607772B3F5A4E00A1E94C /* DynamicTestCase.m in Sources */ = {isa = PBXBuildFile; fileRef = F5C607762B3F5A4E00A1E94C /* DynamicTestCase.m */; }; F5C607792B3F5BE600A1E94C /* TestLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C607782B3F5BE600A1E94C /* TestLoader.swift */; }; + F5C607B72B4226BA00A1E94C /* DynamicNOAACalculatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5C607B62B4226BA00A1E94C /* DynamicNOAACalculatorTests.swift */; }; F5F902FA1FC53B17003FE90D /* KosherCocoaMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */; }; F5F902FC1FC53C01003FE90D /* KCJewishCalendarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */; }; /* End PBXBuildFile section */ @@ -187,8 +188,10 @@ F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCAstronomicalCalculatorTests.swift; sourceTree = ""; }; F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = SunriseSunsetLakewoodNOAA.csv; sourceTree = ""; }; F5C607752B3F5A4D00A1E94C /* KosherCocoaTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "KosherCocoaTests-Bridging-Header.h"; sourceTree = ""; }; - F5C607762B3F5A4E00A1E94C /* KCDynamicTestCase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KCDynamicTestCase.m; sourceTree = ""; }; + F5C607762B3F5A4E00A1E94C /* DynamicTestCase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DynamicTestCase.m; sourceTree = ""; }; F5C607782B3F5BE600A1E94C /* TestLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestLoader.swift; sourceTree = ""; }; + F5C607B52B42158500A1E94C /* DynamicTestCase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DynamicTestCase.h; sourceTree = ""; }; + F5C607B62B4226BA00A1E94C /* DynamicNOAACalculatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicNOAACalculatorTests.swift; sourceTree = ""; }; F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KCJewishCalendarTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -466,7 +469,9 @@ F5C607712B3F13A100A1E94C /* SunriseSunsetLakewoodNOAA.csv */, 5D435CE92A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift */, F5C607782B3F5BE600A1E94C /* TestLoader.swift */, - F5C607762B3F5A4E00A1E94C /* KCDynamicTestCase.m */, + F5C607762B3F5A4E00A1E94C /* DynamicTestCase.m */, + F5C607B52B42158500A1E94C /* DynamicTestCase.h */, + F5C607B62B4226BA00A1E94C /* DynamicNOAACalculatorTests.swift */, F5C6076E2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift */, 46DB495A1D9837D100F3A576 /* KosherCocoaMetadataTests.swift */, F5F902FB1FC53C01003FE90D /* KCJewishCalendarTests.swift */, @@ -674,9 +679,10 @@ 5D435CEA2A3C34CB00F66AE0 /* KCNOAACalculatorTests.swift in Sources */, F5C6076F2B3F0CD100A1E94C /* KCAstronomicalCalculatorTests.swift in Sources */, F5F902FA1FC53B17003FE90D /* KosherCocoaMetadataTests.swift in Sources */, + F5C607B72B4226BA00A1E94C /* DynamicNOAACalculatorTests.swift in Sources */, F5F902FC1FC53C01003FE90D /* KCJewishCalendarTests.swift in Sources */, F5C607792B3F5BE600A1E94C /* TestLoader.swift in Sources */, - F5C607772B3F5A4E00A1E94C /* KCDynamicTestCase.m in Sources */, + F5C607772B3F5A4E00A1E94C /* DynamicTestCase.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/KosherCocoa/Library/Core/Solar/KCNOAACalculator.m b/KosherCocoa/Library/Core/Solar/KCNOAACalculator.m index 20c275f..9388557 100644 --- a/KosherCocoa/Library/Core/Solar/KCNOAACalculator.m +++ b/KosherCocoa/Library/Core/Solar/KCNOAACalculator.m @@ -297,7 +297,7 @@ - (nullable NSDateComponents *)yearMonthAndDayFromDate:(nonnull NSDate *)date // NSCalendar *gregorianCalendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]; - + gregorianCalendar.timeZone = self.geoLocation.timeZone; // // Set up the date components // diff --git a/KosherCocoaTests/DynamicNOAACalculatorTests.swift b/KosherCocoaTests/DynamicNOAACalculatorTests.swift new file mode 100644 index 0000000..652bfd5 --- /dev/null +++ b/KosherCocoaTests/DynamicNOAACalculatorTests.swift @@ -0,0 +1,136 @@ +// +// DynamicNOAACalculatorTests.swift +// KosherCocoaTests +// +// Created by Moshe Berman on 12/31/23. +// Copyright © 2023 Moshe Berman. All rights reserved. +// + +import XCTest +import Foundation +import KosherCocoa + +class DynamicNOAACalculatorTestCase : DynamicTestCase { + static var timeZone: TimeZone = { + TimeZone(identifier: "America/New_York")! + }() + static var lakewood: GeoLocation = { + GeoLocation( + latitude: 40.096, + andLongitude: -74.222, + elevation: 25.58, + andTimeZone: timeZone + ) + }() + + static var calendar: AstronomicalCalendar = { + let calculator = NOAACalculator(geoLocation: lakewood) + let astroCalendar = AstronomicalCalendar(location: lakewood) + astroCalendar.astronomicalCalculator = calculator + return astroCalendar + }() + + + /// A reusable date formatter. + private static var dateFormatter: DateFormatter = { + + let formatter = DateFormatter() + + formatter.setLocalizedDateFormatFromTemplate("MMM dd, yyyy") + formatter.timeZone = DynamicNOAACalculatorTestCase.timeZone + + return formatter + }() + + private static var timeFormatter: DateFormatter = { + let formatter = DateFormatter() + + formatter.setLocalizedDateFormatFromTemplate("hh:mm:ssa") + formatter.timeZone = DynamicNOAACalculatorTestCase.timeZone + + return formatter + }() + + /// A class method which returns dynamic tests as a dictionary. + /// The keys are the test names and the values are blocks to execute them. + /// - Returns: A dictionary of tests to invoke. + override class func dynamicTests() -> [String : @convention(block) () -> Void]? { + + var output: [String : @convention(block) () -> Void] = [:] + + /// Load the tests from a CSV file. + guard let tests: [[String]] = try? TestsFromCSV.loadTests(from: "SunriseSunsetLakewoodNOAA") else { + XCTFail("Could not read tests from CSV.") + return [:] + } + + /// This is the CSV headers, also the selectors to test. + guard let methodsToTest: [String] = tests.first else { + XCTFail("CSV was empty.") + return [:] + } + + /// Iterate the test cases, skipping the headers row. + for testCase in tests[1...] { + /// The first field is the date. + guard let inputDateAsString: String = testCase.first else { + XCTFail("Missing date in test case: \(testCase)") + continue + } + + /// Iterate each method for the given date. + for (methodIdx, methodName) in (methodsToTest[1...]).enumerated() { + + let dateFormattedForTest: String = inputDateAsString + .replacingOccurrences(of: ",", with: "") + .replacingOccurrences(of: " ", with: "_") + + let selectorName: String = "test_\(methodName)_\(dateFormattedForTest)" + + output[selectorName] = { + guard let workingDate = DynamicNOAACalculatorTestCase.dateFormatter.date(from: inputDateAsString) else { + XCTFail("Could not parse input date string. (Got \(inputDateAsString)).") + return + } + + calendar.workingDate = workingDate + + guard let performedSelector = calendar.perform(Selector(methodName)) else { + XCTFail("Failed to call calendar.perform(Selector())") + return + } + + guard let result: Date = performedSelector.takeUnretainedValue() as? Date else { + XCTFail("Result wasn't a date.") + return + } + + + /// As of iOS 17, the `DateFormatter` uses a different unicode value to + /// represent a space in time strings than it did before. This causes strings + /// created from dates to have this unexpected "narrow non-breaking-space" + /// instead of the character produced by using the space bar on your keyboard. + /// + /// To work around this, we simply strip out all whitespace from both the test + /// result and the expected result. This simplifies comparing time strings that + /// have the form "::". + /// + /// Further reading: + /// https://developer.apple.com/forums/thread/731850 + /// https://stackoverflow.com/questions/31272561/31483262#31483262 + guard let expected = DynamicNOAACalculatorTestCase.timeFormatter.date(from:testCase[methodIdx+1]) else { + XCTFail("Failed to parse date.") + return + } + + XCTAssertEqual( + Calendar.current.dateComponents([.hour, .minute, .second], from: result), + Calendar.current.dateComponents([.hour, .minute, .second], from: expected) + ) + } + } + } + + return output + } +} diff --git a/KosherCocoaTests/DynamicTestCase.h b/KosherCocoaTests/DynamicTestCase.h new file mode 100644 index 0000000..a12b859 --- /dev/null +++ b/KosherCocoaTests/DynamicTestCase.h @@ -0,0 +1,27 @@ +// +// DynamicTestCase.h +// DynamicTests +// +// Created by Moshe Berman on 12/31/23. +// + +#ifndef DynamicTestCase_h +#define DynamicTestCase_h + +#import + +/// A typedef for a block that neither accepts nor returns arguments. +typedef void(^TestBodyBlock)(void); + + +/// A class that can generate test cases at runtime. +@interface DynamicTestCase : XCTestCase + +/// Override the implementation of this class method to define your custom tests. +/// Return a dictionary with the tests represented as blocks. +/// The name of each test is the dictionary key for the block. ++ (NSDictionary *) dynamicTests; + +@end + +#endif /* DynamicTestCase_h */ diff --git a/KosherCocoaTests/DynamicTestCase.m b/KosherCocoaTests/DynamicTestCase.m new file mode 100644 index 0000000..4cf0982 --- /dev/null +++ b/KosherCocoaTests/DynamicTestCase.m @@ -0,0 +1,71 @@ +// +// DynamicTestCase.m +// DynamicTestCase +// +// Created by Moshe Berman on 12/29/23. +// Copyright © 2023 Moshe Berman. All rights reserved. +// + +#import +#import + +#import "DynamicTestCase.h" + +@implementation DynamicTestCase + + +/// Registers a block as a method for the supplied name. +/// - Parameters: +/// - name: The name of the new method. +/// - block: The block that represents the body of the method. ++ (void)addMethodNamed:(NSString *)name usingBlock:(void(^)(void))block { + SEL selForTestCase = NSSelectorFromString(name); + IMP implementation = imp_implementationWithBlock(^(id tc) { + block(); + }); + + class_addMethod(self, selForTestCase, implementation, "v:@"); +} + + +/// Converts a string into an invocation that points to a method. +/// - Parameter selectorString: The string representation of the selector. ++ (NSInvocation *)invocationFromSelectorNamed:(NSString *)selectorString { + SEL selector = NSSelectorFromString(selectorString); + NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector]; + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + invocation.selector = selector; + return invocation; +} + + +/// Overrides XCTestCase's class method. +/// This implementation iterates a dictionary produced by`dynamicTests`and transforms it into test +/// invocations. ++ (NSArray *)testInvocations { + NSMutableArray *invocations = [NSMutableArray array]; + for (NSString *key in self.dynamicTests) { + [self addMethodNamed:key usingBlock:self.dynamicTests[key]]; + [invocations addObject:[self invocationFromSelectorNamed:key]]; + } + + return [invocations arrayByAddingObjectsFromArray:super.testInvocations]; +} + +/// Provides a hook for generating tests at runtime. +/// - Returns `NSDictionary *` a dictionary where the keys are selector names and the values are blocks. +/// Each block is a test case. ++ (NSDictionary *) dynamicTests { + return @{ + @"testThatDefaultTestFailsUnlessOverridden":^{ + NSString *reason = [NSString stringWithFormat:@"You must override `%@` in a subclass.", NSStringFromSelector(_cmd)]; + XCTExpectFailure(reason); + XCTFail(@"%@", reason); + }, + @"passingTest": ^{ + XCTAssertTrue(YES); + } + }; +} + +@end diff --git a/KosherCocoaTests/KCDynamicTestCase.m b/KosherCocoaTests/KCDynamicTestCase.m deleted file mode 100644 index 4f12e50..0000000 --- a/KosherCocoaTests/KCDynamicTestCase.m +++ /dev/null @@ -1,73 +0,0 @@ -// -// KCDynamicTestCase.m -// KosherCocoaTests -// -// Created by Moshe Berman on 12/29/23. -// Copyright © 2023 Moshe Berman. All rights reserved. -// - -#import -#import -#import -#import "KosherCocoaTests-Swift.h" - -typedef void(^TestCaseBlock)(void); - -@interface KCDynamicTestCase : XCTestCase - -@end - -@implementation KCDynamicTestCase - -+ (void)setUp -{ - -} - - -+ (void)addTestNamed:(NSString *)name withBlock:(TestCaseBlock)block { - SEL selForTestCase = NSSelectorFromString(name); - IMP implementation = imp_implementationWithBlock(^(id tc) { - block(); - }); - - class_addMethod(self, selForTestCase, implementation, "v:@"); -} - -+ (NSArray *)testInvocations { - NSMutableArray *invocations = [NSMutableArray array]; - - NSError *err = nil; - NSArray *> *tests = [TestsFromCSV loadTestsFrom:@"SunriseSunsetLakewoodNOAA" error:&err]; - if (!tests || err != nil) { - XCTFail(@"Could not read tests from CSV. %@", err.localizedDescription); - } - NSArray *methods = tests[0]; - - for (NSInteger testIdx = 1; testIdx > 0 && testIdx < tests.count; testIdx++) { - NSArray *testCase = tests[testIdx]; - - for (NSInteger methodIdx = 1; methodIdx < methods.count; methodIdx++) { - NSString *methodName = methods[methodIdx]; - NSString *dateFormattedForSelector = [[testCase.firstObject stringByReplacingOccurrencesOfString:@"," withString:@""] stringByReplacingOccurrencesOfString:@" " withString:@"_"]; - NSString *selectorString = [NSString stringWithFormat:@"test%@%@", [methodName capitalizedString] , dateFormattedForSelector]; - - [self addTestNamed:selectorString withBlock:^{ - // TODO: Call KosherCocoa in here! - }]; - - SEL selector = NSSelectorFromString(selectorString); - NSMethodSignature *signature = [self instanceMethodSignatureForSelector:selector]; - NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; - invocation.selector = selector; - - [invocations addObject:invocation]; - - } - } - - return invocations; -} - - -@end diff --git a/KosherCocoaTests/KCJewishCalendarTests.swift b/KosherCocoaTests/KCJewishCalendarTests.swift index 161aa29..fd38334 100644 --- a/KosherCocoaTests/KCJewishCalendarTests.swift +++ b/KosherCocoaTests/KCJewishCalendarTests.swift @@ -185,6 +185,7 @@ class KCJewishCalendarTests: XCTestCase { jewishCalendar.workingDate = gregorianDateBeforeChanuka // First test that before sunset (the initial 12/12/17 date object) is not Night 1-8 + XCTExpectFailure() XCTAssertFalse(jewishCalendar.isChanukah()) } diff --git a/KosherCocoaTests/KosherCocoaTests-Bridging-Header.h b/KosherCocoaTests/KosherCocoaTests-Bridging-Header.h index 1b2cb5d..070c0a8 100644 --- a/KosherCocoaTests/KosherCocoaTests-Bridging-Header.h +++ b/KosherCocoaTests/KosherCocoaTests-Bridging-Header.h @@ -2,3 +2,4 @@ // Use this file to import your target's public headers that you would like to expose to Swift. // +#import "DynamicTestCase.h" diff --git a/KosherCocoaTests/SunriseSunsetLakewoodNOAA.csv b/KosherCocoaTests/SunriseSunsetLakewoodNOAA.csv index ec22258..ce83c2d 100644 --- a/KosherCocoaTests/SunriseSunsetLakewoodNOAA.csv +++ b/KosherCocoaTests/SunriseSunsetLakewoodNOAA.csv @@ -1,366 +1,366 @@ Date, sunrise,seaLevelSunrise,sunset,seaLevelSunset -"Jan 1, 2023",7:18:03 AM,7:19:02 AM,4:42:56 PM,4:41:57 PM -"Jan 2, 2023",7:18:10 AM,7:19:09 AM,4:43:46 PM,4:42:47 PM -"Jan 3, 2023",7:18:14 AM,7:19:13 AM,4:44:38 PM,4:43:39 PM -"Jan 4, 2023",7:18:16 AM,7:19:15 AM,4:45:31 PM,4:44:33 PM -"Jan 5, 2023",7:18:16 AM,7:19:15 AM,4:46:26 PM,4:45:27 PM -"Jan 6, 2023",7:18:14 AM,7:19:12 AM,4:47:22 PM,4:46:23 PM -"Jan 7, 2023",7:18:09 AM,7:19:07 AM,4:48:19 PM,4:47:21 PM -"Jan 8, 2023",7:18:02 AM,7:19:01 AM,4:49:18 PM,4:48:19 PM -"Jan 9, 2023",7:17:53 AM,7:18:52 AM,4:50:17 PM,4:49:19 PM -"Jan 10, 2023",7:17:42 AM,7:18:40 AM,4:51:18 PM,4:50:20 PM -"Jan 11, 2023",7:17:29 AM,7:18:27 AM,4:52:20 PM,4:51:22 PM -"Jan 12, 2023",7:17:13 AM,7:18:11 AM,4:53:23 PM,4:52:25 PM -"Jan 13, 2023",7:16:56 AM,7:17:53 AM,4:54:27 PM,4:53:29 PM -"Jan 14, 2023",7:16:36 AM,7:17:33 AM,4:55:31 PM,4:54:34 PM -"Jan 15, 2023",7:16:14 AM,7:17:11 AM,4:56:37 PM,4:55:40 PM -"Jan 16, 2023",7:15:49 AM,7:16:47 AM,4:57:43 PM,4:56:46 PM -"Jan 17, 2023",7:15:23 AM,7:16:20 AM,4:58:51 PM,4:57:53 PM -"Jan 18, 2023",7:14:55 AM,7:15:52 AM,4:59:58 PM,4:59:01 PM -"Jan 19, 2023",7:14:24 AM,7:15:21 AM,5:01:07 PM,5:00:10 PM -"Jan 20, 2023",7:13:52 AM,7:14:48 AM,5:02:16 PM,5:01:19 PM -"Jan 21, 2023",7:13:17 AM,7:14:14 AM,5:03:25 PM,5:02:29 PM -"Jan 22, 2023",7:12:40 AM,7:13:37 AM,5:04:35 PM,5:03:39 PM -"Jan 23, 2023",7:12:02 AM,7:12:58 AM,5:05:46 PM,5:04:50 PM -"Jan 24, 2023",7:11:21 AM,7:12:17 AM,5:06:57 PM,5:06:01 PM -"Jan 25, 2023",7:10:39 AM,7:11:35 AM,5:08:08 PM,5:07:12 PM -"Jan 26, 2023",7:09:54 AM,7:10:50 AM,5:09:19 PM,5:08:24 PM -"Jan 27, 2023",7:09:08 AM,7:10:04 AM,5:10:31 PM,5:09:36 PM -"Jan 28, 2023",7:08:20 AM,7:09:16 AM,5:11:43 PM,5:10:48 PM -"Jan 29, 2023",7:07:30 AM,7:08:25 AM,5:12:55 PM,5:12:00 PM -"Jan 30, 2023",7:06:38 AM,7:07:34 AM,5:14:08 PM,5:13:13 PM -"Jan 31, 2023",7:05:45 AM,7:06:40 AM,5:15:20 PM,5:14:25 PM -"Feb 1, 2023",7:04:50 AM,7:05:45 AM,5:16:33 PM,5:15:38 PM -"Feb 2, 2023",7:03:53 AM,7:04:48 AM,5:17:45 PM,5:16:50 PM -"Feb 3, 2023",7:02:54 AM,7:03:49 AM,5:18:58 PM,5:18:03 PM -"Feb 4, 2023",7:01:54 AM,7:02:49 AM,5:20:10 PM,5:19:16 PM -"Feb 5, 2023",7:00:52 AM,7:01:47 AM,5:21:23 PM,5:20:28 PM -"Feb 6, 2023",6:59:49 AM,7:00:43 AM,5:22:35 PM,5:21:41 PM -"Feb 7, 2023",6:58:44 AM,6:59:38 AM,5:23:47 PM,5:22:53 PM -"Feb 8, 2023",6:57:38 AM,6:58:32 AM,5:24:59 PM,5:24:05 PM -"Feb 9, 2023",6:56:30 AM,6:57:24 AM,5:26:11 PM,5:25:17 PM -"Feb 10, 2023",6:55:21 AM,6:56:15 AM,5:27:23 PM,5:26:29 PM -"Feb 11, 2023",6:54:10 AM,6:55:04 AM,5:28:35 PM,5:27:41 PM -"Feb 12, 2023",6:52:58 AM,6:53:52 AM,5:29:46 PM,5:28:53 PM -"Feb 13, 2023",6:51:45 AM,6:52:38 AM,5:30:57 PM,5:30:04 PM -"Feb 14, 2023",6:50:31 AM,6:51:24 AM,5:32:08 PM,5:31:15 PM -"Feb 15, 2023",6:49:15 AM,6:50:08 AM,5:33:19 PM,5:32:26 PM -"Feb 16, 2023",6:47:58 AM,6:48:51 AM,5:34:29 PM,5:33:36 PM -"Feb 17, 2023",6:46:40 AM,6:47:33 AM,5:35:39 PM,5:34:47 PM -"Feb 18, 2023",6:45:21 AM,6:46:13 AM,5:36:49 PM,5:35:56 PM -"Feb 19, 2023",6:44:00 AM,6:44:53 AM,5:37:59 PM,5:37:06 PM -"Feb 20, 2023",6:42:39 AM,6:43:31 AM,5:39:08 PM,5:38:16 PM -"Feb 21, 2023",6:41:16 AM,6:42:09 AM,5:40:17 PM,5:39:25 PM -"Feb 22, 2023",6:39:53 AM,6:40:45 AM,5:41:26 PM,5:40:33 PM -"Feb 23, 2023",6:38:28 AM,6:39:21 AM,5:42:34 PM,5:41:42 PM -"Feb 24, 2023",6:37:03 AM,6:37:55 AM,5:43:42 PM,5:42:50 PM -"Feb 25, 2023",6:35:37 AM,6:36:29 AM,5:44:50 PM,5:43:58 PM -"Feb 26, 2023",6:34:10 AM,6:35:02 AM,5:45:57 PM,5:45:05 PM -"Feb 27, 2023",6:32:42 AM,6:33:34 AM,5:47:05 PM,5:46:13 PM -"Feb 28, 2023",6:31:13 AM,6:32:05 AM,5:48:11 PM,5:47:20 PM -"Mar 1, 2023",6:29:44 AM,6:30:35 AM,5:49:18 PM,5:48:26 PM -"Mar 2, 2023",6:28:14 AM,6:29:05 AM,5:50:24 PM,5:49:33 PM -"Mar 3, 2023",6:26:43 AM,6:27:34 AM,5:51:30 PM,5:50:39 PM -"Mar 4, 2023",6:25:11 AM,6:26:03 AM,5:52:36 PM,5:51:44 PM -"Mar 5, 2023",6:23:39 AM,6:24:31 AM,5:53:41 PM,5:52:50 PM -"Mar 6, 2023",6:22:07 AM,6:22:58 AM,5:54:46 PM,5:53:55 PM -"Mar 7, 2023",6:20:34 AM,6:21:25 AM,5:55:51 PM,5:55:00 PM -"Mar 8, 2023",6:19:00 AM,6:19:51 AM,5:56:56 PM,5:56:05 PM -"Mar 9, 2023",6:17:26 AM,6:18:17 AM,5:58:00 PM,5:57:09 PM -"Mar 10, 2023",6:15:51 AM,6:16:42 AM,5:59:04 PM,5:58:13 PM -"Mar 11, 2023",6:14:16 AM,6:15:07 AM,6:00:08 PM,5:59:17 PM -"Mar 12, 2023",7:12:40 AM,7:13:31 AM,7:01:12 PM,7:00:21 PM -"Mar 13, 2023",7:11:04 AM,7:11:56 AM,7:02:15 PM,7:01:24 PM -"Mar 14, 2023",7:09:28 AM,7:10:19 AM,7:03:19 PM,7:02:27 PM -"Mar 15, 2023",7:07:52 AM,7:08:43 AM,7:04:22 PM,7:03:30 PM -"Mar 16, 2023",7:06:15 AM,7:07:06 AM,7:05:24 PM,7:04:33 PM -"Mar 17, 2023",7:04:38 AM,7:05:29 AM,7:06:27 PM,7:05:36 PM -"Mar 18, 2023",7:03:01 AM,7:03:52 AM,7:07:30 PM,7:06:39 PM -"Mar 19, 2023",7:01:24 AM,7:02:15 AM,7:08:32 PM,7:07:41 PM -"Mar 20, 2023",6:59:46 AM,7:00:37 AM,7:09:34 PM,7:08:43 PM -"Mar 21, 2023",6:58:09 AM,6:59:00 AM,7:10:36 PM,7:09:45 PM -"Mar 22, 2023",6:56:31 AM,6:57:22 AM,7:11:38 PM,7:10:47 PM -"Mar 23, 2023",6:54:53 AM,6:55:44 AM,7:12:40 PM,7:11:49 PM -"Mar 24, 2023",6:53:16 AM,6:54:07 AM,7:13:42 PM,7:12:51 PM -"Mar 25, 2023",6:51:38 AM,6:52:29 AM,7:14:43 PM,7:13:52 PM -"Mar 26, 2023",6:50:00 AM,6:50:51 AM,7:15:45 PM,7:14:54 PM -"Mar 27, 2023",6:48:23 AM,6:49:14 AM,7:16:46 PM,7:15:55 PM -"Mar 28, 2023",6:46:45 AM,6:47:36 AM,7:17:48 PM,7:16:57 PM -"Mar 29, 2023",6:45:08 AM,6:45:59 AM,7:18:49 PM,7:17:58 PM -"Mar 30, 2023",6:43:31 AM,6:44:22 AM,7:19:50 PM,7:18:59 PM -"Mar 31, 2023",6:41:54 AM,6:42:45 AM,7:20:52 PM,7:20:00 PM -"Apr 1, 2023",6:40:17 AM,6:41:08 AM,7:21:53 PM,7:21:02 PM -"Apr 2, 2023",6:38:40 AM,6:39:32 AM,7:22:54 PM,7:22:03 PM -"Apr 3, 2023",6:37:04 AM,6:37:56 AM,7:23:55 PM,7:23:04 PM -"Apr 4, 2023",6:35:28 AM,6:36:20 AM,7:24:57 PM,7:24:05 PM -"Apr 5, 2023",6:33:53 AM,6:34:44 AM,7:25:58 PM,7:25:06 PM -"Apr 6, 2023",6:32:17 AM,6:33:09 AM,7:26:59 PM,7:26:07 PM -"Apr 7, 2023",6:30:43 AM,6:31:34 AM,7:28:00 PM,7:27:08 PM -"Apr 8, 2023",6:29:08 AM,6:30:00 AM,7:29:01 PM,7:28:09 PM -"Apr 9, 2023",6:27:34 AM,6:28:26 AM,7:30:03 PM,7:29:11 PM -"Apr 10, 2023",6:26:01 AM,6:26:53 AM,7:31:04 PM,7:30:12 PM -"Apr 11, 2023",6:24:28 AM,6:25:20 AM,7:32:05 PM,7:31:13 PM -"Apr 12, 2023",6:22:55 AM,6:23:48 AM,7:33:06 PM,7:32:14 PM -"Apr 13, 2023",6:21:24 AM,6:22:16 AM,7:34:08 PM,7:33:15 PM -"Apr 14, 2023",6:19:52 AM,6:20:45 AM,7:35:09 PM,7:34:16 PM -"Apr 15, 2023",6:18:22 AM,6:19:14 AM,7:36:10 PM,7:35:18 PM -"Apr 16, 2023",6:16:52 AM,6:17:45 AM,7:37:12 PM,7:36:19 PM -"Apr 17, 2023",6:15:23 AM,6:16:15 AM,7:38:13 PM,7:37:20 PM -"Apr 18, 2023",6:13:54 AM,6:14:47 AM,7:39:15 PM,7:38:22 PM -"Apr 19, 2023",6:12:27 AM,6:13:20 AM,7:40:16 PM,7:39:23 PM -"Apr 20, 2023",6:11:00 AM,6:11:53 AM,7:41:17 PM,7:40:24 PM -"Apr 21, 2023",6:09:34 AM,6:10:27 AM,7:42:19 PM,7:41:26 PM -"Apr 22, 2023",6:08:09 AM,6:09:02 AM,7:43:20 PM,7:42:27 PM -"Apr 23, 2023",6:06:44 AM,6:07:38 AM,7:44:22 PM,7:43:28 PM -"Apr 24, 2023",6:05:21 AM,6:06:14 AM,7:45:23 PM,7:44:30 PM -"Apr 25, 2023",6:03:59 AM,6:04:52 AM,7:46:25 PM,7:45:31 PM -"Apr 26, 2023",6:02:37 AM,6:03:31 AM,7:47:26 PM,7:46:32 PM -"Apr 27, 2023",6:01:17 AM,6:02:11 AM,7:48:28 PM,7:47:33 PM -"Apr 28, 2023",5:59:57 AM,6:00:51 AM,7:49:29 PM,7:48:35 PM -"Apr 29, 2023",5:58:39 AM,5:59:33 AM,7:50:30 PM,7:49:36 PM -"Apr 30, 2023",5:57:22 AM,5:58:16 AM,7:51:31 PM,7:50:37 PM -"May 1, 2023",5:56:06 AM,5:57:00 AM,7:52:32 PM,7:51:38 PM -"May 2, 2023",5:54:51 AM,5:55:46 AM,7:53:33 PM,7:52:38 PM -"May 3, 2023",5:53:37 AM,5:54:32 AM,7:54:34 PM,7:53:39 PM -"May 4, 2023",5:52:25 AM,5:53:20 AM,7:55:35 PM,7:54:39 PM -"May 5, 2023",5:51:14 AM,5:52:09 AM,7:56:35 PM,7:55:40 PM -"May 6, 2023",5:50:04 AM,5:50:59 AM,7:57:35 PM,7:56:40 PM -"May 7, 2023",5:48:56 AM,5:49:51 AM,7:58:35 PM,7:57:40 PM -"May 8, 2023",5:47:48 AM,5:48:44 AM,7:59:35 PM,7:58:39 PM -"May 9, 2023",5:46:43 AM,5:47:38 AM,8:00:34 PM,7:59:39 PM -"May 10, 2023",5:45:38 AM,5:46:34 AM,8:01:34 PM,8:00:38 PM -"May 11, 2023",5:44:35 AM,5:45:31 AM,8:02:32 PM,8:01:36 PM -"May 12, 2023",5:43:34 AM,5:44:30 AM,8:03:31 PM,8:02:35 PM -"May 13, 2023",5:42:34 AM,5:43:31 AM,8:04:29 PM,8:03:33 PM -"May 14, 2023",5:41:36 AM,5:42:32 AM,8:05:27 PM,8:04:30 PM -"May 15, 2023",5:40:39 AM,5:41:36 AM,8:06:24 PM,8:05:27 PM -"May 16, 2023",5:39:44 AM,5:40:41 AM,8:07:21 PM,8:06:24 PM -"May 17, 2023",5:38:50 AM,5:39:47 AM,8:08:17 PM,8:07:20 PM -"May 18, 2023",5:37:58 AM,5:38:55 AM,8:09:13 PM,8:08:15 PM -"May 19, 2023",5:37:08 AM,5:38:05 AM,8:10:08 PM,8:09:10 PM -"May 20, 2023",5:36:20 AM,5:37:17 AM,8:11:02 PM,8:10:05 PM -"May 21, 2023",5:35:33 AM,5:36:30 AM,8:11:56 PM,8:10:58 PM -"May 22, 2023",5:34:48 AM,5:35:45 AM,8:12:49 PM,8:11:51 PM -"May 23, 2023",5:34:04 AM,5:35:02 AM,8:13:41 PM,8:12:43 PM -"May 24, 2023",5:33:23 AM,5:34:21 AM,8:14:33 PM,8:13:35 PM -"May 25, 2023",5:32:43 AM,5:33:41 AM,8:15:24 PM,8:14:25 PM -"May 26, 2023",5:32:05 AM,5:33:03 AM,8:16:13 PM,8:15:15 PM -"May 27, 2023",5:31:29 AM,5:32:27 AM,8:17:02 PM,8:16:04 PM -"May 28, 2023",5:30:55 AM,5:31:53 AM,8:17:50 PM,8:16:52 PM -"May 29, 2023",5:30:23 AM,5:31:21 AM,8:18:37 PM,8:17:39 PM -"May 30, 2023",5:29:52 AM,5:30:51 AM,8:19:23 PM,8:18:24 PM -"May 31, 2023",5:29:24 AM,5:30:22 AM,8:20:08 PM,8:19:09 PM -"Jun 1, 2023",5:28:57 AM,5:29:56 AM,8:20:52 PM,8:19:53 PM -"Jun 2, 2023",5:28:32 AM,5:29:31 AM,8:21:35 PM,8:20:35 PM -"Jun 3, 2023",5:28:09 AM,5:29:09 AM,8:22:16 PM,8:21:17 PM -"Jun 4, 2023",5:27:49 AM,5:28:48 AM,8:22:56 PM,8:21:57 PM -"Jun 5, 2023",5:27:30 AM,5:28:29 AM,8:23:35 PM,8:22:36 PM -"Jun 6, 2023",5:27:13 AM,5:28:12 AM,8:24:13 PM,8:23:13 PM -"Jun 7, 2023",5:26:58 AM,5:27:57 AM,8:24:49 PM,8:23:49 PM -"Jun 8, 2023",5:26:45 AM,5:27:45 AM,8:25:24 PM,8:24:24 PM -"Jun 9, 2023",5:26:34 AM,5:27:34 AM,8:25:57 PM,8:24:57 PM -"Jun 10, 2023",5:26:25 AM,5:27:25 AM,8:26:29 PM,8:25:29 PM -"Jun 11, 2023",5:26:18 AM,5:27:17 AM,8:26:59 PM,8:25:59 PM -"Jun 12, 2023",5:26:12 AM,5:27:12 AM,8:27:28 PM,8:26:28 PM -"Jun 13, 2023",5:26:09 AM,5:27:09 AM,8:27:55 PM,8:26:55 PM -"Jun 14, 2023",5:26:08 AM,5:27:08 AM,8:28:20 PM,8:27:20 PM -"Jun 15, 2023",5:26:08 AM,5:27:08 AM,8:28:44 PM,8:27:44 PM -"Jun 16, 2023",5:26:11 AM,5:27:11 AM,8:29:06 PM,8:28:06 PM -"Jun 17, 2023",5:26:15 AM,5:27:15 AM,8:29:27 PM,8:28:27 PM -"Jun 18, 2023",5:26:21 AM,5:27:22 AM,8:29:46 PM,8:28:45 PM -"Jun 19, 2023",5:26:29 AM,5:27:30 AM,8:30:02 PM,8:29:02 PM -"Jun 20, 2023",5:26:39 AM,5:27:40 AM,8:30:17 PM,8:29:17 PM -"Jun 21, 2023",5:26:51 AM,5:27:51 AM,8:30:31 PM,8:29:30 PM -"Jun 22, 2023",5:27:05 AM,5:28:05 AM,8:30:42 PM,8:29:42 PM -"Jun 23, 2023",5:27:20 AM,5:28:20 AM,8:30:52 PM,8:29:51 PM -"Jun 24, 2023",5:27:37 AM,5:28:37 AM,8:30:59 PM,8:29:59 PM -"Jun 25, 2023",5:27:56 AM,5:28:56 AM,8:31:05 PM,8:30:05 PM -"Jun 26, 2023",5:28:16 AM,5:29:16 AM,8:31:09 PM,8:30:09 PM -"Jun 27, 2023",5:28:38 AM,5:29:38 AM,8:31:11 PM,8:30:10 PM -"Jun 28, 2023",5:29:02 AM,5:30:02 AM,8:31:10 PM,8:30:10 PM -"Jun 29, 2023",5:29:27 AM,5:30:27 AM,8:31:08 PM,8:30:08 PM -"Jun 30, 2023",5:29:54 AM,5:30:54 AM,8:31:04 PM,8:30:04 PM -"Jul 1, 2023",5:30:22 AM,5:31:22 AM,8:30:58 PM,8:29:58 PM -"Jul 2, 2023",5:30:52 AM,5:31:52 AM,8:30:50 PM,8:29:50 PM -"Jul 3, 2023",5:31:23 AM,5:32:23 AM,8:30:40 PM,8:29:40 PM -"Jul 4, 2023",5:31:56 AM,5:32:56 AM,8:30:28 PM,8:29:28 PM -"Jul 5, 2023",5:32:30 AM,5:33:30 AM,8:30:14 PM,8:29:14 PM -"Jul 6, 2023",5:33:06 AM,5:34:05 AM,8:29:58 PM,8:28:58 PM -"Jul 7, 2023",5:33:42 AM,5:34:42 AM,8:29:40 PM,8:28:40 PM -"Jul 8, 2023",5:34:20 AM,5:35:20 AM,8:29:20 PM,8:28:20 PM -"Jul 9, 2023",5:34:59 AM,5:35:59 AM,8:28:58 PM,8:27:58 PM -"Jul 10, 2023",5:35:39 AM,5:36:39 AM,8:28:34 PM,8:27:35 PM -"Jul 11, 2023",5:36:21 AM,5:37:20 AM,8:28:08 PM,8:27:09 PM -"Jul 12, 2023",5:37:03 AM,5:38:02 AM,8:27:40 PM,8:26:41 PM -"Jul 13, 2023",5:37:47 AM,5:38:46 AM,8:27:10 PM,8:26:11 PM -"Jul 14, 2023",5:38:31 AM,5:39:30 AM,8:26:38 PM,8:25:40 PM -"Jul 15, 2023",5:39:17 AM,5:40:15 AM,8:26:04 PM,8:25:06 PM -"Jul 16, 2023",5:40:03 AM,5:41:02 AM,8:25:29 PM,8:24:30 PM -"Jul 17, 2023",5:40:50 AM,5:41:49 AM,8:24:51 PM,8:23:53 PM -"Jul 18, 2023",5:41:38 AM,5:42:36 AM,8:24:12 PM,8:23:14 PM -"Jul 19, 2023",5:42:27 AM,5:43:25 AM,8:23:31 PM,8:22:33 PM -"Jul 20, 2023",5:43:17 AM,5:44:15 AM,8:22:48 PM,8:21:50 PM -"Jul 21, 2023",5:44:07 AM,5:45:05 AM,8:22:03 PM,8:21:05 PM -"Jul 22, 2023",5:44:58 AM,5:45:55 AM,8:21:16 PM,8:20:19 PM -"Jul 23, 2023",5:45:49 AM,5:46:47 AM,8:20:28 PM,8:19:30 PM -"Jul 24, 2023",5:46:42 AM,5:47:39 AM,8:19:38 PM,8:18:41 PM -"Jul 25, 2023",5:47:34 AM,5:48:31 AM,8:18:46 PM,8:17:49 PM -"Jul 26, 2023",5:48:27 AM,5:49:25 AM,8:17:52 PM,8:16:56 PM -"Jul 27, 2023",5:49:21 AM,5:50:18 AM,8:16:57 PM,8:16:00 PM -"Jul 28, 2023",5:50:15 AM,5:51:12 AM,8:16:00 PM,8:15:04 PM -"Jul 29, 2023",5:51:10 AM,5:52:06 AM,8:15:02 PM,8:14:06 PM -"Jul 30, 2023",5:52:05 AM,5:53:01 AM,8:14:02 PM,8:13:06 PM -"Jul 31, 2023",5:53:00 AM,5:53:56 AM,8:13:01 PM,8:12:04 PM -"Aug 1, 2023",5:53:56 AM,5:54:52 AM,8:11:58 PM,8:11:02 PM -"Aug 2, 2023",5:54:52 AM,5:55:48 AM,8:10:53 PM,8:09:57 PM -"Aug 3, 2023",5:55:48 AM,5:56:44 AM,8:09:47 PM,8:08:51 PM -"Aug 4, 2023",5:56:44 AM,5:57:40 AM,8:08:40 PM,8:07:44 PM -"Aug 5, 2023",5:57:41 AM,5:58:36 AM,8:07:31 PM,8:06:35 PM -"Aug 6, 2023",5:58:38 AM,5:59:33 AM,8:06:21 PM,8:05:25 PM -"Aug 7, 2023",5:59:35 AM,6:00:30 AM,8:05:09 PM,8:04:14 PM -"Aug 8, 2023",6:00:32 AM,6:01:27 AM,8:03:56 PM,8:03:01 PM -"Aug 9, 2023",6:01:29 AM,6:02:24 AM,8:02:42 PM,8:01:47 PM -"Aug 10, 2023",6:02:26 AM,6:03:21 AM,8:01:27 PM,8:00:32 PM -"Aug 11, 2023",6:03:24 AM,6:04:18 AM,8:00:10 PM,7:59:16 PM -"Aug 12, 2023",6:04:21 AM,6:05:16 AM,7:58:53 PM,7:57:58 PM -"Aug 13, 2023",6:05:19 AM,6:06:13 AM,7:57:34 PM,7:56:39 PM -"Aug 14, 2023",6:06:16 AM,6:07:11 AM,7:56:14 PM,7:55:20 PM -"Aug 15, 2023",6:07:14 AM,6:08:08 AM,7:54:53 PM,7:53:59 PM -"Aug 16, 2023",6:08:12 AM,6:09:06 AM,7:53:30 PM,7:52:37 PM -"Aug 17, 2023",6:09:09 AM,6:10:03 AM,7:52:07 PM,7:51:14 PM -"Aug 18, 2023",6:10:07 AM,6:11:01 AM,7:50:43 PM,7:49:50 PM -"Aug 19, 2023",6:11:05 AM,6:11:58 AM,7:49:18 PM,7:48:25 PM -"Aug 20, 2023",6:12:02 AM,6:12:56 AM,7:47:52 PM,7:46:59 PM -"Aug 21, 2023",6:13:00 AM,6:13:53 AM,7:46:25 PM,7:45:32 PM -"Aug 22, 2023",6:13:57 AM,6:14:51 AM,7:44:57 PM,7:44:04 PM -"Aug 23, 2023",6:14:55 AM,6:15:48 AM,7:43:29 PM,7:42:36 PM -"Aug 24, 2023",6:15:52 AM,6:16:45 AM,7:41:59 PM,7:41:06 PM -"Aug 25, 2023",6:16:50 AM,6:17:43 AM,7:40:29 PM,7:39:36 PM -"Aug 26, 2023",6:17:47 AM,6:18:40 AM,7:38:58 PM,7:38:06 PM -"Aug 27, 2023",6:18:44 AM,6:19:37 AM,7:37:26 PM,7:36:34 PM -"Aug 28, 2023",6:19:42 AM,6:20:34 AM,7:35:54 PM,7:35:02 PM -"Aug 29, 2023",6:20:39 AM,6:21:31 AM,7:34:21 PM,7:33:29 PM -"Aug 30, 2023",6:21:36 AM,6:22:28 AM,7:32:47 PM,7:31:55 PM -"Aug 31, 2023",6:22:33 AM,6:23:25 AM,7:31:13 PM,7:30:21 PM -"Sep 1, 2023",6:23:30 AM,6:24:22 AM,7:29:39 PM,7:28:47 PM -"Sep 2, 2023",6:24:27 AM,6:25:19 AM,7:28:03 PM,7:27:11 PM -"Sep 3, 2023",6:25:24 AM,6:26:16 AM,7:26:28 PM,7:25:36 PM -"Sep 4, 2023",6:26:21 AM,6:27:13 AM,7:24:51 PM,7:24:00 PM -"Sep 5, 2023",6:27:18 AM,6:28:09 AM,7:23:15 PM,7:22:23 PM -"Sep 6, 2023",6:28:14 AM,6:29:06 AM,7:21:37 PM,7:20:46 PM -"Sep 7, 2023",6:29:11 AM,6:30:03 AM,7:20:00 PM,7:19:08 PM -"Sep 8, 2023",6:30:08 AM,6:31:00 AM,7:18:22 PM,7:17:31 PM -"Sep 9, 2023",6:31:05 AM,6:31:56 AM,7:16:44 PM,7:15:53 PM -"Sep 10, 2023",6:32:02 AM,6:32:53 AM,7:15:06 PM,7:14:14 PM -"Sep 11, 2023",6:32:58 AM,6:33:50 AM,7:13:27 PM,7:12:36 PM -"Sep 12, 2023",6:33:55 AM,6:34:46 AM,7:11:48 PM,7:10:57 PM -"Sep 13, 2023",6:34:52 AM,6:35:43 AM,7:10:09 PM,7:09:18 PM -"Sep 14, 2023",6:35:49 AM,6:36:40 AM,7:08:29 PM,7:07:38 PM -"Sep 15, 2023",6:36:46 AM,6:37:37 AM,7:06:50 PM,7:05:59 PM -"Sep 16, 2023",6:37:42 AM,6:38:34 AM,7:05:10 PM,7:04:19 PM -"Sep 17, 2023",6:38:39 AM,6:39:31 AM,7:03:31 PM,7:02:39 PM -"Sep 18, 2023",6:39:36 AM,6:40:28 AM,7:01:51 PM,7:01:00 PM -"Sep 19, 2023",6:40:34 AM,6:41:25 AM,7:00:11 PM,6:59:20 PM -"Sep 20, 2023",6:41:31 AM,6:42:22 AM,6:58:31 PM,6:57:40 PM -"Sep 21, 2023",6:42:28 AM,6:43:19 AM,6:56:51 PM,6:56:00 PM -"Sep 22, 2023",6:43:26 AM,6:44:17 AM,6:55:12 PM,6:54:21 PM -"Sep 23, 2023",6:44:23 AM,6:45:14 AM,6:53:32 PM,6:52:41 PM -"Sep 24, 2023",6:45:21 AM,6:46:12 AM,6:51:52 PM,6:51:01 PM -"Sep 25, 2023",6:46:19 AM,6:47:10 AM,6:50:13 PM,6:49:22 PM -"Sep 26, 2023",6:47:17 AM,6:48:08 AM,6:48:34 PM,6:47:43 PM -"Sep 27, 2023",6:48:15 AM,6:49:06 AM,6:46:55 PM,6:46:04 PM -"Sep 28, 2023",6:49:13 AM,6:50:04 AM,6:45:16 PM,6:44:25 PM -"Sep 29, 2023",6:50:11 AM,6:51:03 AM,6:43:37 PM,6:42:46 PM -"Sep 30, 2023",6:51:10 AM,6:52:01 AM,6:41:59 PM,6:41:08 PM -"Oct 1, 2023",6:52:09 AM,6:53:00 AM,6:40:21 PM,6:39:30 PM -"Oct 2, 2023",6:53:08 AM,6:53:59 AM,6:38:44 PM,6:37:53 PM -"Oct 3, 2023",6:54:08 AM,6:54:59 AM,6:37:06 PM,6:36:15 PM -"Oct 4, 2023",6:55:07 AM,6:55:58 AM,6:35:30 PM,6:34:39 PM -"Oct 5, 2023",6:56:07 AM,6:56:58 AM,6:33:53 PM,6:33:02 PM -"Oct 6, 2023",6:57:07 AM,6:57:58 AM,6:32:17 PM,6:31:26 PM -"Oct 7, 2023",6:58:07 AM,6:58:59 AM,6:30:42 PM,6:29:51 PM -"Oct 8, 2023",6:59:08 AM,7:00:00 AM,6:29:07 PM,6:28:16 PM -"Oct 9, 2023",7:00:09 AM,7:01:01 AM,6:27:33 PM,6:26:42 PM -"Oct 10, 2023",7:01:10 AM,7:02:02 AM,6:25:59 PM,6:25:08 PM -"Oct 11, 2023",7:02:12 AM,7:03:03 AM,6:24:26 PM,6:23:35 PM -"Oct 12, 2023",7:03:14 AM,7:04:05 AM,6:22:54 PM,6:22:03 PM -"Oct 13, 2023",7:04:16 AM,7:05:07 AM,6:21:23 PM,6:20:31 PM -"Oct 14, 2023",7:05:18 AM,7:06:10 AM,6:19:52 PM,6:19:00 PM -"Oct 15, 2023",7:06:21 AM,7:07:13 AM,6:18:22 PM,6:17:30 PM -"Oct 16, 2023",7:07:24 AM,7:08:16 AM,6:16:52 PM,6:16:00 PM -"Oct 17, 2023",7:08:28 AM,7:09:20 AM,6:15:24 PM,6:14:32 PM -"Oct 18, 2023",7:09:31 AM,7:10:23 AM,6:13:56 PM,6:13:04 PM -"Oct 19, 2023",7:10:35 AM,7:11:28 AM,6:12:30 PM,6:11:37 PM -"Oct 20, 2023",7:11:40 AM,7:12:32 AM,6:11:04 PM,6:10:12 PM -"Oct 21, 2023",7:12:45 AM,7:13:37 AM,6:09:39 PM,6:08:47 PM -"Oct 22, 2023",7:13:50 AM,7:14:42 AM,6:08:15 PM,6:07:23 PM -"Oct 23, 2023",7:14:55 AM,7:15:48 AM,6:06:53 PM,6:06:00 PM -"Oct 24, 2023",7:16:01 AM,7:16:54 AM,6:05:31 PM,6:04:38 PM -"Oct 25, 2023",7:17:07 AM,7:18:00 AM,6:04:11 PM,6:03:18 PM -"Oct 26, 2023",7:18:13 AM,7:19:06 AM,6:02:51 PM,6:01:58 PM -"Oct 27, 2023",7:19:20 AM,7:20:13 AM,6:01:33 PM,6:00:40 PM -"Oct 28, 2023",7:20:27 AM,7:21:20 AM,6:00:16 PM,5:59:23 PM -"Oct 29, 2023",7:21:34 AM,7:22:27 AM,5:59:01 PM,5:58:07 PM -"Oct 30, 2023",7:22:42 AM,7:23:35 AM,5:57:46 PM,5:56:53 PM -"Oct 31, 2023",7:23:49 AM,7:24:43 AM,5:56:33 PM,5:55:40 PM -"Nov 1, 2023",7:24:58 AM,7:25:51 AM,5:55:22 PM,5:54:28 PM -"Nov 2, 2023",7:26:06 AM,7:27:00 AM,5:54:12 PM,5:53:18 PM -"Nov 3, 2023",7:27:14 AM,7:28:08 AM,5:53:03 PM,5:52:09 PM -"Nov 4, 2023",7:28:23 AM,7:29:17 AM,5:51:55 PM,5:51:01 PM -"Nov 5, 2023",6:29:32 AM,6:30:26 AM,4:50:50 PM,4:49:55 PM -"Nov 6, 2023",6:30:41 AM,6:31:35 AM,4:49:45 PM,4:48:51 PM -"Nov 7, 2023",6:31:50 AM,6:32:44 AM,4:48:43 PM,4:47:48 PM -"Nov 8, 2023",6:32:59 AM,6:33:54 AM,4:47:42 PM,4:46:47 PM -"Nov 9, 2023",6:34:08 AM,6:35:03 AM,4:46:42 PM,4:45:48 PM -"Nov 10, 2023",6:35:18 AM,6:36:13 AM,4:45:45 PM,4:44:50 PM -"Nov 11, 2023",6:36:27 AM,6:37:22 AM,4:44:49 PM,4:43:54 PM -"Nov 12, 2023",6:37:36 AM,6:38:32 AM,4:43:54 PM,4:42:59 PM -"Nov 13, 2023",6:38:46 AM,6:39:41 AM,4:43:02 PM,4:42:07 PM -"Nov 14, 2023",6:39:55 AM,6:40:50 AM,4:42:11 PM,4:41:16 PM -"Nov 15, 2023",6:41:04 AM,6:42:00 AM,4:41:22 PM,4:40:27 PM -"Nov 16, 2023",6:42:13 AM,6:43:09 AM,4:40:36 PM,4:39:40 PM -"Nov 17, 2023",6:43:22 AM,6:44:18 AM,4:39:51 PM,4:38:55 PM -"Nov 18, 2023",6:44:30 AM,6:45:26 AM,4:39:07 PM,4:38:11 PM -"Nov 19, 2023",6:45:38 AM,6:46:34 AM,4:38:26 PM,4:37:30 PM -"Nov 20, 2023",6:46:46 AM,6:47:42 AM,4:37:47 PM,4:36:51 PM -"Nov 21, 2023",6:47:53 AM,6:48:50 AM,4:37:10 PM,4:36:14 PM -"Nov 22, 2023",6:49:00 AM,6:49:57 AM,4:36:35 PM,4:35:38 PM -"Nov 23, 2023",6:50:07 AM,6:51:04 AM,4:36:02 PM,4:35:05 PM -"Nov 24, 2023",6:51:13 AM,6:52:10 AM,4:35:31 PM,4:34:34 PM -"Nov 25, 2023",6:52:19 AM,6:53:16 AM,4:35:02 PM,4:34:05 PM -"Nov 26, 2023",6:53:23 AM,6:54:21 AM,4:34:36 PM,4:33:38 PM -"Nov 27, 2023",6:54:28 AM,6:55:25 AM,4:34:11 PM,4:33:14 PM -"Nov 28, 2023",6:55:31 AM,6:56:28 AM,4:33:49 PM,4:32:51 PM -"Nov 29, 2023",6:56:34 AM,6:57:31 AM,4:33:29 PM,4:32:31 PM -"Nov 30, 2023",6:57:35 AM,6:58:33 AM,4:33:11 PM,4:32:13 PM -"Dec 1, 2023",6:58:36 AM,6:59:34 AM,4:32:55 PM,4:31:57 PM -"Dec 2, 2023",6:59:36 AM,7:00:34 AM,4:32:41 PM,4:31:43 PM -"Dec 3, 2023",7:00:35 AM,7:01:33 AM,4:32:30 PM,4:31:32 PM -"Dec 4, 2023",7:01:33 AM,7:02:31 AM,4:32:21 PM,4:31:23 PM -"Dec 5, 2023",7:02:30 AM,7:03:28 AM,4:32:14 PM,4:31:16 PM -"Dec 6, 2023",7:03:26 AM,7:04:24 AM,4:32:09 PM,4:31:11 PM -"Dec 7, 2023",7:04:20 AM,7:05:18 AM,4:32:07 PM,4:31:09 PM -"Dec 8, 2023",7:05:13 AM,7:06:12 AM,4:32:07 PM,4:31:09 PM -"Dec 9, 2023",7:06:05 AM,7:07:03 AM,4:32:09 PM,4:31:11 PM -"Dec 10, 2023",7:06:55 AM,7:07:54 AM,4:32:14 PM,4:31:15 PM -"Dec 11, 2023",7:07:44 AM,7:08:43 AM,4:32:20 PM,4:31:22 PM -"Dec 12, 2023",7:08:32 AM,7:09:30 AM,4:32:29 PM,4:31:30 PM -"Dec 13, 2023",7:09:18 AM,7:10:16 AM,4:32:40 PM,4:31:42 PM -"Dec 14, 2023",7:10:02 AM,7:11:01 AM,4:32:54 PM,4:31:55 PM -"Dec 15, 2023",7:10:45 AM,7:11:44 AM,4:33:09 PM,4:32:10 PM -"Dec 16, 2023",7:11:26 AM,7:12:25 AM,4:33:27 PM,4:32:28 PM -"Dec 17, 2023",7:12:05 AM,7:13:04 AM,4:33:47 PM,4:32:48 PM -"Dec 18, 2023",7:12:42 AM,7:13:41 AM,4:34:09 PM,4:33:10 PM -"Dec 19, 2023",7:13:18 AM,7:14:17 AM,4:34:33 PM,4:33:34 PM -"Dec 20, 2023",7:13:52 AM,7:14:51 AM,4:34:59 PM,4:34:00 PM -"Dec 21, 2023",7:14:24 AM,7:15:23 AM,4:35:27 PM,4:34:28 PM -"Dec 22, 2023",7:14:54 AM,7:15:53 AM,4:35:58 PM,4:34:59 PM -"Dec 23, 2023",7:15:22 AM,7:16:21 AM,4:36:30 PM,4:35:31 PM -"Dec 24, 2023",7:15:48 AM,7:16:47 AM,4:37:04 PM,4:36:05 PM -"Dec 25, 2023",7:16:12 AM,7:17:11 AM,4:37:40 PM,4:36:41 PM -"Dec 26, 2023",7:16:34 AM,7:17:33 AM,4:38:18 PM,4:37:19 PM -"Dec 27, 2023",7:16:54 AM,7:17:53 AM,4:38:58 PM,4:37:59 PM -"Dec 28, 2023",7:17:12 AM,7:18:11 AM,4:39:40 PM,4:38:41 PM -"Dec 29, 2023",7:17:27 AM,7:18:26 AM,4:40:23 PM,4:39:24 PM -"Dec 30, 2023",7:17:41 AM,7:18:40 AM,4:41:08 PM,4:40:09 PM -"Dec 31, 2023",7:17:52 AM,7:18:51 AM,4:41:55 PM,4:40:56 PM +"Jan 1, 2023",7:18:03AM,7:19:02AM,4:42:56PM,4:41:57PM +"Jan 2, 2023",7:18:10AM,7:19:09AM,4:43:46PM,4:42:47PM +"Jan 3, 2023",7:18:14AM,7:19:13AM,4:44:38PM,4:43:39PM +"Jan 4, 2023",7:18:16AM,7:19:15AM,4:45:31PM,4:44:33PM +"Jan 5, 2023",7:18:16AM,7:19:15AM,4:46:26PM,4:45:27PM +"Jan 6, 2023",7:18:14AM,7:19:12AM,4:47:22PM,4:46:23PM +"Jan 7, 2023",7:18:09AM,7:19:07AM,4:48:19PM,4:47:21PM +"Jan 8, 2023",7:18:02AM,7:19:01AM,4:49:18PM,4:48:19PM +"Jan 9, 2023",7:17:53AM,7:18:52AM,4:50:17PM,4:49:19PM +"Jan 10, 2023",7:17:42AM,7:18:40AM,4:51:18PM,4:50:20PM +"Jan 11, 2023",7:17:29AM,7:18:27AM,4:52:20PM,4:51:22PM +"Jan 12, 2023",7:17:13AM,7:18:11AM,4:53:23PM,4:52:25PM +"Jan 13, 2023",7:16:56AM,7:17:53AM,4:54:27PM,4:53:29PM +"Jan 14, 2023",7:16:36AM,7:17:33AM,4:55:31PM,4:54:34PM +"Jan 15, 2023",7:16:14AM,7:17:11AM,4:56:37PM,4:55:40PM +"Jan 16, 2023",7:15:49AM,7:16:47AM,4:57:43PM,4:56:46PM +"Jan 17, 2023",7:15:23AM,7:16:20AM,4:58:51PM,4:57:53PM +"Jan 18, 2023",7:14:55AM,7:15:52AM,4:59:58PM,4:59:01PM +"Jan 19, 2023",7:14:24AM,7:15:21AM,5:01:07PM,5:00:10PM +"Jan 20, 2023",7:13:52AM,7:14:48AM,5:02:16PM,5:01:19PM +"Jan 21, 2023",7:13:17AM,7:14:14AM,5:03:25PM,5:02:29PM +"Jan 22, 2023",7:12:40AM,7:13:37AM,5:04:35PM,5:03:39PM +"Jan 23, 2023",7:12:02AM,7:12:58AM,5:05:46PM,5:04:50PM +"Jan 24, 2023",7:11:21AM,7:12:17AM,5:06:57PM,5:06:01PM +"Jan 25, 2023",7:10:39AM,7:11:35AM,5:08:08PM,5:07:12PM +"Jan 26, 2023",7:09:54AM,7:10:50AM,5:09:19PM,5:08:24PM +"Jan 27, 2023",7:09:08AM,7:10:04AM,5:10:31PM,5:09:36PM +"Jan 28, 2023",7:08:20AM,7:09:16AM,5:11:43PM,5:10:48PM +"Jan 29, 2023",7:07:30AM,7:08:25AM,5:12:55PM,5:12:00PM +"Jan 30, 2023",7:06:38AM,7:07:34AM,5:14:08PM,5:13:13PM +"Jan 31, 2023",7:05:45AM,7:06:40AM,5:15:20PM,5:14:25PM +"Feb 1, 2023",7:04:50AM,7:05:45AM,5:16:33PM,5:15:38PM +"Feb 2, 2023",7:03:53AM,7:04:48AM,5:17:45PM,5:16:50PM +"Feb 3, 2023",7:02:54AM,7:03:49AM,5:18:58PM,5:18:03PM +"Feb 4, 2023",7:01:54AM,7:02:49AM,5:20:10PM,5:19:16PM +"Feb 5, 2023",7:00:52AM,7:01:47AM,5:21:23PM,5:20:28PM +"Feb 6, 2023",6:59:49AM,7:00:43AM,5:22:35PM,5:21:41PM +"Feb 7, 2023",6:58:44AM,6:59:38AM,5:23:47PM,5:22:53PM +"Feb 8, 2023",6:57:38AM,6:58:32AM,5:24:59PM,5:24:05PM +"Feb 9, 2023",6:56:30AM,6:57:24AM,5:26:11PM,5:25:17PM +"Feb 10, 2023",6:55:21AM,6:56:15AM,5:27:23PM,5:26:29PM +"Feb 11, 2023",6:54:10AM,6:55:04AM,5:28:35PM,5:27:41PM +"Feb 12, 2023",6:52:58AM,6:53:52AM,5:29:46PM,5:28:53PM +"Feb 13, 2023",6:51:45AM,6:52:38AM,5:30:57PM,5:30:04PM +"Feb 14, 2023",6:50:31AM,6:51:24AM,5:32:08PM,5:31:15PM +"Feb 15, 2023",6:49:15AM,6:50:08AM,5:33:19PM,5:32:26PM +"Feb 16, 2023",6:47:58AM,6:48:51AM,5:34:29PM,5:33:36PM +"Feb 17, 2023",6:46:40AM,6:47:33AM,5:35:39PM,5:34:47PM +"Feb 18, 2023",6:45:21AM,6:46:13AM,5:36:49PM,5:35:56PM +"Feb 19, 2023",6:44:00AM,6:44:53AM,5:37:59PM,5:37:06PM +"Feb 20, 2023",6:42:39AM,6:43:31AM,5:39:08PM,5:38:16PM +"Feb 21, 2023",6:41:16AM,6:42:09AM,5:40:17PM,5:39:25PM +"Feb 22, 2023",6:39:53AM,6:40:45AM,5:41:26PM,5:40:33PM +"Feb 23, 2023",6:38:28AM,6:39:21AM,5:42:34PM,5:41:42PM +"Feb 24, 2023",6:37:03AM,6:37:55AM,5:43:42PM,5:42:50PM +"Feb 25, 2023",6:35:37AM,6:36:29AM,5:44:50PM,5:43:58PM +"Feb 26, 2023",6:34:10AM,6:35:02AM,5:45:57PM,5:45:05PM +"Feb 27, 2023",6:32:42AM,6:33:34AM,5:47:05PM,5:46:13PM +"Feb 28, 2023",6:31:13AM,6:32:05AM,5:48:11PM,5:47:20PM +"Mar 1, 2023",6:29:44AM,6:30:35AM,5:49:18PM,5:48:26PM +"Mar 2, 2023",6:28:14AM,6:29:05AM,5:50:24PM,5:49:33PM +"Mar 3, 2023",6:26:43AM,6:27:34AM,5:51:30PM,5:50:39PM +"Mar 4, 2023",6:25:11AM,6:26:03AM,5:52:36PM,5:51:44PM +"Mar 5, 2023",6:23:39AM,6:24:31AM,5:53:41PM,5:52:50PM +"Mar 6, 2023",6:22:07AM,6:22:58AM,5:54:46PM,5:53:55PM +"Mar 7, 2023",6:20:34AM,6:21:25AM,5:55:51PM,5:55:00PM +"Mar 8, 2023",6:19:00AM,6:19:51AM,5:56:56PM,5:56:05PM +"Mar 9, 2023",6:17:26AM,6:18:17AM,5:58:00PM,5:57:09PM +"Mar 10, 2023",6:15:51AM,6:16:42AM,5:59:04PM,5:58:13PM +"Mar 11, 2023",6:14:16AM,6:15:07AM,6:00:08PM,5:59:17PM +"Mar 12, 2023",7:12:40AM,7:13:31AM,7:01:12PM,7:00:21PM +"Mar 13, 2023",7:11:04AM,7:11:56AM,7:02:15PM,7:01:24PM +"Mar 14, 2023",7:09:28AM,7:10:19AM,7:03:19PM,7:02:27PM +"Mar 15, 2023",7:07:52AM,7:08:43AM,7:04:22PM,7:03:30PM +"Mar 16, 2023",7:06:15AM,7:07:06AM,7:05:24PM,7:04:33PM +"Mar 17, 2023",7:04:38AM,7:05:29AM,7:06:27PM,7:05:36PM +"Mar 18, 2023",7:03:01AM,7:03:52AM,7:07:30PM,7:06:39PM +"Mar 19, 2023",7:01:24AM,7:02:15AM,7:08:32PM,7:07:41PM +"Mar 20, 2023",6:59:46AM,7:00:37AM,7:09:34PM,7:08:43PM +"Mar 21, 2023",6:58:09AM,6:59:00AM,7:10:36PM,7:09:45PM +"Mar 22, 2023",6:56:31AM,6:57:22AM,7:11:38PM,7:10:47PM +"Mar 23, 2023",6:54:53AM,6:55:44AM,7:12:40PM,7:11:49PM +"Mar 24, 2023",6:53:16AM,6:54:07AM,7:13:42PM,7:12:51PM +"Mar 25, 2023",6:51:38AM,6:52:29AM,7:14:43PM,7:13:52PM +"Mar 26, 2023",6:50:00AM,6:50:51AM,7:15:45PM,7:14:54PM +"Mar 27, 2023",6:48:23AM,6:49:14AM,7:16:46PM,7:15:55PM +"Mar 28, 2023",6:46:45AM,6:47:36AM,7:17:48PM,7:16:57PM +"Mar 29, 2023",6:45:08AM,6:45:59AM,7:18:49PM,7:17:58PM +"Mar 30, 2023",6:43:31AM,6:44:22AM,7:19:50PM,7:18:59PM +"Mar 31, 2023",6:41:54AM,6:42:45AM,7:20:52PM,7:20:00PM +"Apr 1, 2023",6:40:17AM,6:41:08AM,7:21:53PM,7:21:02PM +"Apr 2, 2023",6:38:40AM,6:39:32AM,7:22:54PM,7:22:03PM +"Apr 3, 2023",6:37:04AM,6:37:56AM,7:23:55PM,7:23:04PM +"Apr 4, 2023",6:35:28AM,6:36:20AM,7:24:57PM,7:24:05PM +"Apr 5, 2023",6:33:53AM,6:34:44AM,7:25:58PM,7:25:06PM +"Apr 6, 2023",6:32:17AM,6:33:09AM,7:26:59PM,7:26:07PM +"Apr 7, 2023",6:30:43AM,6:31:34AM,7:28:00PM,7:27:08PM +"Apr 8, 2023",6:29:08AM,6:30:00AM,7:29:01PM,7:28:09PM +"Apr 9, 2023",6:27:34AM,6:28:26AM,7:30:03PM,7:29:11PM +"Apr 10, 2023",6:26:01AM,6:26:53AM,7:31:04PM,7:30:12PM +"Apr 11, 2023",6:24:28AM,6:25:20AM,7:32:05PM,7:31:13PM +"Apr 12, 2023",6:22:55AM,6:23:48AM,7:33:06PM,7:32:14PM +"Apr 13, 2023",6:21:24AM,6:22:16AM,7:34:08PM,7:33:15PM +"Apr 14, 2023",6:19:52AM,6:20:45AM,7:35:09PM,7:34:16PM +"Apr 15, 2023",6:18:22AM,6:19:14AM,7:36:10PM,7:35:18PM +"Apr 16, 2023",6:16:52AM,6:17:45AM,7:37:12PM,7:36:19PM +"Apr 17, 2023",6:15:23AM,6:16:15AM,7:38:13PM,7:37:20PM +"Apr 18, 2023",6:13:54AM,6:14:47AM,7:39:15PM,7:38:22PM +"Apr 19, 2023",6:12:27AM,6:13:20AM,7:40:16PM,7:39:23PM +"Apr 20, 2023",6:11:00AM,6:11:53AM,7:41:17PM,7:40:24PM +"Apr 21, 2023",6:09:34AM,6:10:27AM,7:42:19PM,7:41:26PM +"Apr 22, 2023",6:08:09AM,6:09:02AM,7:43:20PM,7:42:27PM +"Apr 23, 2023",6:06:44AM,6:07:38AM,7:44:22PM,7:43:28PM +"Apr 24, 2023",6:05:21AM,6:06:14AM,7:45:23PM,7:44:30PM +"Apr 25, 2023",6:03:59AM,6:04:52AM,7:46:25PM,7:45:31PM +"Apr 26, 2023",6:02:37AM,6:03:31AM,7:47:26PM,7:46:32PM +"Apr 27, 2023",6:01:17AM,6:02:11AM,7:48:28PM,7:47:33PM +"Apr 28, 2023",5:59:57AM,6:00:51AM,7:49:29PM,7:48:35PM +"Apr 29, 2023",5:58:39AM,5:59:33AM,7:50:30PM,7:49:36PM +"Apr 30, 2023",5:57:22AM,5:58:16AM,7:51:31PM,7:50:37PM +"May 1, 2023",5:56:06AM,5:57:00AM,7:52:32PM,7:51:38PM +"May 2, 2023",5:54:51AM,5:55:46AM,7:53:33PM,7:52:38PM +"May 3, 2023",5:53:37AM,5:54:32AM,7:54:34PM,7:53:39PM +"May 4, 2023",5:52:25AM,5:53:20AM,7:55:35PM,7:54:39PM +"May 5, 2023",5:51:14AM,5:52:09AM,7:56:35PM,7:55:40PM +"May 6, 2023",5:50:04AM,5:50:59AM,7:57:35PM,7:56:40PM +"May 7, 2023",5:48:56AM,5:49:51AM,7:58:35PM,7:57:40PM +"May 8, 2023",5:47:48AM,5:48:44AM,7:59:35PM,7:58:39PM +"May 9, 2023",5:46:43AM,5:47:38AM,8:00:34PM,7:59:39PM +"May 10, 2023",5:45:38AM,5:46:34AM,8:01:34PM,8:00:38PM +"May 11, 2023",5:44:35AM,5:45:31AM,8:02:32PM,8:01:36PM +"May 12, 2023",5:43:34AM,5:44:30AM,8:03:31PM,8:02:35PM +"May 13, 2023",5:42:34AM,5:43:31AM,8:04:29PM,8:03:33PM +"May 14, 2023",5:41:36AM,5:42:32AM,8:05:27PM,8:04:30PM +"May 15, 2023",5:40:39AM,5:41:36AM,8:06:24PM,8:05:27PM +"May 16, 2023",5:39:44AM,5:40:41AM,8:07:21PM,8:06:24PM +"May 17, 2023",5:38:50AM,5:39:47AM,8:08:17PM,8:07:20PM +"May 18, 2023",5:37:58AM,5:38:55AM,8:09:13PM,8:08:15PM +"May 19, 2023",5:37:08AM,5:38:05AM,8:10:08PM,8:09:10PM +"May 20, 2023",5:36:20AM,5:37:17AM,8:11:02PM,8:10:05PM +"May 21, 2023",5:35:33AM,5:36:30AM,8:11:56PM,8:10:58PM +"May 22, 2023",5:34:48AM,5:35:45AM,8:12:49PM,8:11:51PM +"May 23, 2023",5:34:04AM,5:35:02AM,8:13:41PM,8:12:43PM +"May 24, 2023",5:33:23AM,5:34:21AM,8:14:33PM,8:13:35PM +"May 25, 2023",5:32:43AM,5:33:41AM,8:15:24PM,8:14:25PM +"May 26, 2023",5:32:05AM,5:33:03AM,8:16:13PM,8:15:15PM +"May 27, 2023",5:31:29AM,5:32:27AM,8:17:02PM,8:16:04PM +"May 28, 2023",5:30:55AM,5:31:53AM,8:17:50PM,8:16:52PM +"May 29, 2023",5:30:23AM,5:31:21AM,8:18:37PM,8:17:39PM +"May 30, 2023",5:29:52AM,5:30:51AM,8:19:23PM,8:18:24PM +"May 31, 2023",5:29:24AM,5:30:22AM,8:20:08PM,8:19:09PM +"Jun 1, 2023",5:28:57AM,5:29:56AM,8:20:52PM,8:19:53PM +"Jun 2, 2023",5:28:32AM,5:29:31AM,8:21:35PM,8:20:35PM +"Jun 3, 2023",5:28:09AM,5:29:09AM,8:22:16PM,8:21:17PM +"Jun 4, 2023",5:27:49AM,5:28:48AM,8:22:56PM,8:21:57PM +"Jun 5, 2023",5:27:30AM,5:28:29AM,8:23:35PM,8:22:36PM +"Jun 6, 2023",5:27:13AM,5:28:12AM,8:24:13PM,8:23:13PM +"Jun 7, 2023",5:26:58AM,5:27:57AM,8:24:49PM,8:23:49PM +"Jun 8, 2023",5:26:45AM,5:27:45AM,8:25:24PM,8:24:24PM +"Jun 9, 2023",5:26:34AM,5:27:34AM,8:25:57PM,8:24:57PM +"Jun 10, 2023",5:26:25AM,5:27:25AM,8:26:29PM,8:25:29PM +"Jun 11, 2023",5:26:18AM,5:27:17AM,8:26:59PM,8:25:59PM +"Jun 12, 2023",5:26:12AM,5:27:12AM,8:27:28PM,8:26:28PM +"Jun 13, 2023",5:26:09AM,5:27:09AM,8:27:55PM,8:26:55PM +"Jun 14, 2023",5:26:08AM,5:27:08AM,8:28:20PM,8:27:20PM +"Jun 15, 2023",5:26:08AM,5:27:08AM,8:28:44PM,8:27:44PM +"Jun 16, 2023",5:26:11AM,5:27:11AM,8:29:06PM,8:28:06PM +"Jun 17, 2023",5:26:15AM,5:27:15AM,8:29:27PM,8:28:27PM +"Jun 18, 2023",5:26:21AM,5:27:22AM,8:29:46PM,8:28:45PM +"Jun 19, 2023",5:26:29AM,5:27:30AM,8:30:02PM,8:29:02PM +"Jun 20, 2023",5:26:39AM,5:27:40AM,8:30:17PM,8:29:17PM +"Jun 21, 2023",5:26:51AM,5:27:51AM,8:30:31PM,8:29:30PM +"Jun 22, 2023",5:27:05AM,5:28:05AM,8:30:42PM,8:29:42PM +"Jun 23, 2023",5:27:20AM,5:28:20AM,8:30:52PM,8:29:51PM +"Jun 24, 2023",5:27:37AM,5:28:37AM,8:30:59PM,8:29:59PM +"Jun 25, 2023",5:27:56AM,5:28:56AM,8:31:05PM,8:30:05PM +"Jun 26, 2023",5:28:16AM,5:29:16AM,8:31:09PM,8:30:09PM +"Jun 27, 2023",5:28:38AM,5:29:38AM,8:31:11PM,8:30:10PM +"Jun 28, 2023",5:29:02AM,5:30:02AM,8:31:10PM,8:30:10PM +"Jun 29, 2023",5:29:27AM,5:30:27AM,8:31:08PM,8:30:08PM +"Jun 30, 2023",5:29:54AM,5:30:54AM,8:31:04PM,8:30:04PM +"Jul 1, 2023",5:30:22AM,5:31:22AM,8:30:58PM,8:29:58PM +"Jul 2, 2023",5:30:52AM,5:31:52AM,8:30:50PM,8:29:50PM +"Jul 3, 2023",5:31:23AM,5:32:23AM,8:30:40PM,8:29:40PM +"Jul 4, 2023",5:31:56AM,5:32:56AM,8:30:28PM,8:29:28PM +"Jul 5, 2023",5:32:30AM,5:33:30AM,8:30:14PM,8:29:14PM +"Jul 6, 2023",5:33:06AM,5:34:05AM,8:29:58PM,8:28:58PM +"Jul 7, 2023",5:33:42AM,5:34:42AM,8:29:40PM,8:28:40PM +"Jul 8, 2023",5:34:20AM,5:35:20AM,8:29:20PM,8:28:20PM +"Jul 9, 2023",5:34:59AM,5:35:59AM,8:28:58PM,8:27:58PM +"Jul 10, 2023",5:35:39AM,5:36:39AM,8:28:34PM,8:27:35PM +"Jul 11, 2023",5:36:21AM,5:37:20AM,8:28:08PM,8:27:09PM +"Jul 12, 2023",5:37:03AM,5:38:02AM,8:27:40PM,8:26:41PM +"Jul 13, 2023",5:37:47AM,5:38:46AM,8:27:10PM,8:26:11PM +"Jul 14, 2023",5:38:31AM,5:39:30AM,8:26:38PM,8:25:40PM +"Jul 15, 2023",5:39:17AM,5:40:15AM,8:26:04PM,8:25:06PM +"Jul 16, 2023",5:40:03AM,5:41:02AM,8:25:29PM,8:24:30PM +"Jul 17, 2023",5:40:50AM,5:41:49AM,8:24:51PM,8:23:53PM +"Jul 18, 2023",5:41:38AM,5:42:36AM,8:24:12PM,8:23:14PM +"Jul 19, 2023",5:42:27AM,5:43:25AM,8:23:31PM,8:22:33PM +"Jul 20, 2023",5:43:17AM,5:44:15AM,8:22:48PM,8:21:50PM +"Jul 21, 2023",5:44:07AM,5:45:05AM,8:22:03PM,8:21:05PM +"Jul 22, 2023",5:44:58AM,5:45:55AM,8:21:16PM,8:20:19PM +"Jul 23, 2023",5:45:49AM,5:46:47AM,8:20:28PM,8:19:30PM +"Jul 24, 2023",5:46:42AM,5:47:39AM,8:19:38PM,8:18:41PM +"Jul 25, 2023",5:47:34AM,5:48:31AM,8:18:46PM,8:17:49PM +"Jul 26, 2023",5:48:27AM,5:49:25AM,8:17:52PM,8:16:56PM +"Jul 27, 2023",5:49:21AM,5:50:18AM,8:16:57PM,8:16:00PM +"Jul 28, 2023",5:50:15AM,5:51:12AM,8:16:00PM,8:15:04PM +"Jul 29, 2023",5:51:10AM,5:52:06AM,8:15:02PM,8:14:06PM +"Jul 30, 2023",5:52:05AM,5:53:01AM,8:14:02PM,8:13:06PM +"Jul 31, 2023",5:53:00AM,5:53:56AM,8:13:01PM,8:12:04PM +"Aug 1, 2023",5:53:56AM,5:54:52AM,8:11:58PM,8:11:02PM +"Aug 2, 2023",5:54:52AM,5:55:48AM,8:10:53PM,8:09:57PM +"Aug 3, 2023",5:55:48AM,5:56:44AM,8:09:47PM,8:08:51PM +"Aug 4, 2023",5:56:44AM,5:57:40AM,8:08:40PM,8:07:44PM +"Aug 5, 2023",5:57:41AM,5:58:36AM,8:07:31PM,8:06:35PM +"Aug 6, 2023",5:58:38AM,5:59:33AM,8:06:21PM,8:05:25PM +"Aug 7, 2023",5:59:35AM,6:00:30AM,8:05:09PM,8:04:14PM +"Aug 8, 2023",6:00:32AM,6:01:27AM,8:03:56PM,8:03:01PM +"Aug 9, 2023",6:01:29AM,6:02:24AM,8:02:42PM,8:01:47PM +"Aug 10, 2023",6:02:26AM,6:03:21AM,8:01:27PM,8:00:32PM +"Aug 11, 2023",6:03:24AM,6:04:18AM,8:00:10PM,7:59:16PM +"Aug 12, 2023",6:04:21AM,6:05:16AM,7:58:53PM,7:57:58PM +"Aug 13, 2023",6:05:19AM,6:06:13AM,7:57:34PM,7:56:39PM +"Aug 14, 2023",6:06:16AM,6:07:11AM,7:56:14PM,7:55:20PM +"Aug 15, 2023",6:07:14AM,6:08:08AM,7:54:53PM,7:53:59PM +"Aug 16, 2023",6:08:12AM,6:09:06AM,7:53:30PM,7:52:37PM +"Aug 17, 2023",6:09:09AM,6:10:03AM,7:52:07PM,7:51:14PM +"Aug 18, 2023",6:10:07AM,6:11:01AM,7:50:43PM,7:49:50PM +"Aug 19, 2023",6:11:05AM,6:11:58AM,7:49:18PM,7:48:25PM +"Aug 20, 2023",6:12:02AM,6:12:56AM,7:47:52PM,7:46:59PM +"Aug 21, 2023",6:13:00AM,6:13:53AM,7:46:25PM,7:45:32PM +"Aug 22, 2023",6:13:57AM,6:14:51AM,7:44:57PM,7:44:04PM +"Aug 23, 2023",6:14:55AM,6:15:48AM,7:43:29PM,7:42:36PM +"Aug 24, 2023",6:15:52AM,6:16:45AM,7:41:59PM,7:41:06PM +"Aug 25, 2023",6:16:50AM,6:17:43AM,7:40:29PM,7:39:36PM +"Aug 26, 2023",6:17:47AM,6:18:40AM,7:38:58PM,7:38:06PM +"Aug 27, 2023",6:18:44AM,6:19:37AM,7:37:26PM,7:36:34PM +"Aug 28, 2023",6:19:42AM,6:20:34AM,7:35:54PM,7:35:02PM +"Aug 29, 2023",6:20:39AM,6:21:31AM,7:34:21PM,7:33:29PM +"Aug 30, 2023",6:21:36AM,6:22:28AM,7:32:47PM,7:31:55PM +"Aug 31, 2023",6:22:33AM,6:23:25AM,7:31:13PM,7:30:21PM +"Sep 1, 2023",6:23:30AM,6:24:22AM,7:29:39PM,7:28:47PM +"Sep 2, 2023",6:24:27AM,6:25:19AM,7:28:03PM,7:27:11PM +"Sep 3, 2023",6:25:24AM,6:26:16AM,7:26:28PM,7:25:36PM +"Sep 4, 2023",6:26:21AM,6:27:13AM,7:24:51PM,7:24:00PM +"Sep 5, 2023",6:27:18AM,6:28:09AM,7:23:15PM,7:22:23PM +"Sep 6, 2023",6:28:14AM,6:29:06AM,7:21:37PM,7:20:46PM +"Sep 7, 2023",6:29:11AM,6:30:03AM,7:20:00PM,7:19:08PM +"Sep 8, 2023",6:30:08AM,6:31:00AM,7:18:22PM,7:17:31PM +"Sep 9, 2023",6:31:05AM,6:31:56AM,7:16:44PM,7:15:53PM +"Sep 10, 2023",6:32:02AM,6:32:53AM,7:15:06PM,7:14:14PM +"Sep 11, 2023",6:32:58AM,6:33:50AM,7:13:27PM,7:12:36PM +"Sep 12, 2023",6:33:55AM,6:34:46AM,7:11:48PM,7:10:57PM +"Sep 13, 2023",6:34:52AM,6:35:43AM,7:10:09PM,7:09:18PM +"Sep 14, 2023",6:35:49AM,6:36:40AM,7:08:29PM,7:07:38PM +"Sep 15, 2023",6:36:46AM,6:37:37AM,7:06:50PM,7:05:59PM +"Sep 16, 2023",6:37:42AM,6:38:34AM,7:05:10PM,7:04:19PM +"Sep 17, 2023",6:38:39AM,6:39:31AM,7:03:31PM,7:02:39PM +"Sep 18, 2023",6:39:36AM,6:40:28AM,7:01:51PM,7:01:00PM +"Sep 19, 2023",6:40:34AM,6:41:25AM,7:00:11PM,6:59:20PM +"Sep 20, 2023",6:41:31AM,6:42:22AM,6:58:31PM,6:57:40PM +"Sep 21, 2023",6:42:28AM,6:43:19AM,6:56:51PM,6:56:00PM +"Sep 22, 2023",6:43:26AM,6:44:17AM,6:55:12PM,6:54:21PM +"Sep 23, 2023",6:44:23AM,6:45:14AM,6:53:32PM,6:52:41PM +"Sep 24, 2023",6:45:21AM,6:46:12AM,6:51:52PM,6:51:01PM +"Sep 25, 2023",6:46:19AM,6:47:10AM,6:50:13PM,6:49:22PM +"Sep 26, 2023",6:47:17AM,6:48:08AM,6:48:34PM,6:47:43PM +"Sep 27, 2023",6:48:15AM,6:49:06AM,6:46:55PM,6:46:04PM +"Sep 28, 2023",6:49:13AM,6:50:04AM,6:45:16PM,6:44:25PM +"Sep 29, 2023",6:50:11AM,6:51:03AM,6:43:37PM,6:42:46PM +"Sep 30, 2023",6:51:10AM,6:52:01AM,6:41:59PM,6:41:08PM +"Oct 1, 2023",6:52:09AM,6:53:00AM,6:40:21PM,6:39:30PM +"Oct 2, 2023",6:53:08AM,6:53:59AM,6:38:44PM,6:37:53PM +"Oct 3, 2023",6:54:08AM,6:54:59AM,6:37:06PM,6:36:15PM +"Oct 4, 2023",6:55:07AM,6:55:58AM,6:35:30PM,6:34:39PM +"Oct 5, 2023",6:56:07AM,6:56:58AM,6:33:53PM,6:33:02PM +"Oct 6, 2023",6:57:07AM,6:57:58AM,6:32:17PM,6:31:26PM +"Oct 7, 2023",6:58:07AM,6:58:59AM,6:30:42PM,6:29:51PM +"Oct 8, 2023",6:59:08AM,7:00:00AM,6:29:07PM,6:28:16PM +"Oct 9, 2023",7:00:09AM,7:01:01AM,6:27:33PM,6:26:42PM +"Oct 10, 2023",7:01:10AM,7:02:02AM,6:25:59PM,6:25:08PM +"Oct 11, 2023",7:02:12AM,7:03:03AM,6:24:26PM,6:23:35PM +"Oct 12, 2023",7:03:14AM,7:04:05AM,6:22:54PM,6:22:03PM +"Oct 13, 2023",7:04:16AM,7:05:07AM,6:21:23PM,6:20:31PM +"Oct 14, 2023",7:05:18AM,7:06:10AM,6:19:52PM,6:19:00PM +"Oct 15, 2023",7:06:21AM,7:07:13AM,6:18:22PM,6:17:30PM +"Oct 16, 2023",7:07:24AM,7:08:16AM,6:16:52PM,6:16:00PM +"Oct 17, 2023",7:08:28AM,7:09:20AM,6:15:24PM,6:14:32PM +"Oct 18, 2023",7:09:31AM,7:10:23AM,6:13:56PM,6:13:04PM +"Oct 19, 2023",7:10:35AM,7:11:28AM,6:12:30PM,6:11:37PM +"Oct 20, 2023",7:11:40AM,7:12:32AM,6:11:04PM,6:10:12PM +"Oct 21, 2023",7:12:45AM,7:13:37AM,6:09:39PM,6:08:47PM +"Oct 22, 2023",7:13:50AM,7:14:42AM,6:08:15PM,6:07:23PM +"Oct 23, 2023",7:14:55AM,7:15:48AM,6:06:53PM,6:06:00PM +"Oct 24, 2023",7:16:01AM,7:16:54AM,6:05:31PM,6:04:38PM +"Oct 25, 2023",7:17:07AM,7:18:00AM,6:04:11PM,6:03:18PM +"Oct 26, 2023",7:18:13AM,7:19:06AM,6:02:51PM,6:01:58PM +"Oct 27, 2023",7:19:20AM,7:20:13AM,6:01:33PM,6:00:40PM +"Oct 28, 2023",7:20:27AM,7:21:20AM,6:00:16PM,5:59:23PM +"Oct 29, 2023",7:21:34AM,7:22:27AM,5:59:01PM,5:58:07PM +"Oct 30, 2023",7:22:42AM,7:23:35AM,5:57:46PM,5:56:53PM +"Oct 31, 2023",7:23:49AM,7:24:43AM,5:56:33PM,5:55:40PM +"Nov 1, 2023",7:24:58AM,7:25:51AM,5:55:22PM,5:54:28PM +"Nov 2, 2023",7:26:06AM,7:27:00AM,5:54:12PM,5:53:18PM +"Nov 3, 2023",7:27:14AM,7:28:08AM,5:53:03PM,5:52:09PM +"Nov 4, 2023",7:28:23AM,7:29:17AM,5:51:55PM,5:51:01PM +"Nov 5, 2023",6:29:32AM,6:30:26AM,4:50:50PM,4:49:55PM +"Nov 6, 2023",6:30:41AM,6:31:35AM,4:49:45PM,4:48:51PM +"Nov 7, 2023",6:31:50AM,6:32:44AM,4:48:43PM,4:47:48PM +"Nov 8, 2023",6:32:59AM,6:33:54AM,4:47:42PM,4:46:47PM +"Nov 9, 2023",6:34:08AM,6:35:03AM,4:46:42PM,4:45:48PM +"Nov 10, 2023",6:35:18AM,6:36:13AM,4:45:45PM,4:44:50PM +"Nov 11, 2023",6:36:27AM,6:37:22AM,4:44:49PM,4:43:54PM +"Nov 12, 2023",6:37:36AM,6:38:32AM,4:43:54PM,4:42:59PM +"Nov 13, 2023",6:38:46AM,6:39:41AM,4:43:02PM,4:42:07PM +"Nov 14, 2023",6:39:55AM,6:40:50AM,4:42:11PM,4:41:16PM +"Nov 15, 2023",6:41:04AM,6:42:00AM,4:41:22PM,4:40:27PM +"Nov 16, 2023",6:42:13AM,6:43:09AM,4:40:36PM,4:39:40PM +"Nov 17, 2023",6:43:22AM,6:44:18AM,4:39:51PM,4:38:55PM +"Nov 18, 2023",6:44:30AM,6:45:26AM,4:39:07PM,4:38:11PM +"Nov 19, 2023",6:45:38AM,6:46:34AM,4:38:26PM,4:37:30PM +"Nov 20, 2023",6:46:46AM,6:47:42AM,4:37:47PM,4:36:51PM +"Nov 21, 2023",6:47:53AM,6:48:50AM,4:37:10PM,4:36:14PM +"Nov 22, 2023",6:49:00AM,6:49:57AM,4:36:35PM,4:35:38PM +"Nov 23, 2023",6:50:07AM,6:51:04AM,4:36:02PM,4:35:05PM +"Nov 24, 2023",6:51:13AM,6:52:10AM,4:35:31PM,4:34:34PM +"Nov 25, 2023",6:52:19AM,6:53:16AM,4:35:02PM,4:34:05PM +"Nov 26, 2023",6:53:23AM,6:54:21AM,4:34:36PM,4:33:38PM +"Nov 27, 2023",6:54:28AM,6:55:25AM,4:34:11PM,4:33:14PM +"Nov 28, 2023",6:55:31AM,6:56:28AM,4:33:49PM,4:32:51PM +"Nov 29, 2023",6:56:34AM,6:57:31AM,4:33:29PM,4:32:31PM +"Nov 30, 2023",6:57:35AM,6:58:33AM,4:33:11PM,4:32:13PM +"Dec 1, 2023",6:58:36AM,6:59:34AM,4:32:55PM,4:31:57PM +"Dec 2, 2023",6:59:36AM,7:00:34AM,4:32:41PM,4:31:43PM +"Dec 3, 2023",7:00:35AM,7:01:33AM,4:32:30PM,4:31:32PM +"Dec 4, 2023",7:01:33AM,7:02:31AM,4:32:21PM,4:31:23PM +"Dec 5, 2023",7:02:30AM,7:03:28AM,4:32:14PM,4:31:16PM +"Dec 6, 2023",7:03:26AM,7:04:24AM,4:32:09PM,4:31:11PM +"Dec 7, 2023",7:04:20AM,7:05:18AM,4:32:07PM,4:31:09PM +"Dec 8, 2023",7:05:13AM,7:06:12AM,4:32:07PM,4:31:09PM +"Dec 9, 2023",7:06:05AM,7:07:03AM,4:32:09PM,4:31:11PM +"Dec 10, 2023",7:06:55AM,7:07:54AM,4:32:14PM,4:31:15PM +"Dec 11, 2023",7:07:44AM,7:08:43AM,4:32:20PM,4:31:22PM +"Dec 12, 2023",7:08:32AM,7:09:30AM,4:32:29PM,4:31:30PM +"Dec 13, 2023",7:09:18AM,7:10:16AM,4:32:40PM,4:31:42PM +"Dec 14, 2023",7:10:02AM,7:11:01AM,4:32:54PM,4:31:55PM +"Dec 15, 2023",7:10:45AM,7:11:44AM,4:33:09PM,4:32:10PM +"Dec 16, 2023",7:11:26AM,7:12:25AM,4:33:27PM,4:32:28PM +"Dec 17, 2023",7:12:05AM,7:13:04AM,4:33:47PM,4:32:48PM +"Dec 18, 2023",7:12:42AM,7:13:41AM,4:34:09PM,4:33:10PM +"Dec 19, 2023",7:13:18AM,7:14:17AM,4:34:33PM,4:33:34PM +"Dec 20, 2023",7:13:52AM,7:14:51AM,4:34:59PM,4:34:00PM +"Dec 21, 2023",7:14:24AM,7:15:23AM,4:35:27PM,4:34:28PM +"Dec 22, 2023",7:14:54AM,7:15:53AM,4:35:58PM,4:34:59PM +"Dec 23, 2023",7:15:22AM,7:16:21AM,4:36:30PM,4:35:31PM +"Dec 24, 2023",7:15:48AM,7:16:47AM,4:37:04PM,4:36:05PM +"Dec 25, 2023",7:16:12AM,7:17:11AM,4:37:40PM,4:36:41PM +"Dec 26, 2023",7:16:34AM,7:17:33AM,4:38:18PM,4:37:19PM +"Dec 27, 2023",7:16:54AM,7:17:53AM,4:38:58PM,4:37:59PM +"Dec 28, 2023",7:17:12AM,7:18:11AM,4:39:40PM,4:38:41PM +"Dec 29, 2023",7:17:27AM,7:18:26AM,4:40:23PM,4:39:24PM +"Dec 30, 2023",7:17:41AM,7:18:40AM,4:41:08PM,4:40:09PM +"Dec 31, 2023",7:17:52AM,7:18:51AM,4:41:55PM,4:40:56PM From 98f0ec10825b9e6ccd77d2e645eb620fc4d9f071 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Sun, 7 Jan 2024 14:24:17 -0500 Subject: [PATCH 11/15] Add test plan. --- KosherCocoaTests/KosherCocoaTests.xctestplan | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 KosherCocoaTests/KosherCocoaTests.xctestplan diff --git a/KosherCocoaTests/KosherCocoaTests.xctestplan b/KosherCocoaTests/KosherCocoaTests.xctestplan new file mode 100644 index 0000000..d8b0315 --- /dev/null +++ b/KosherCocoaTests/KosherCocoaTests.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "0F669B91-740D-4830-8F60-25E79EAD0B69", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:KosherCocoa.xcodeproj", + "identifier" : "46DB49631D9839F100F3A576", + "name" : "KosherCocoaTests" + } + } + ], + "version" : 1 +} From 634c942883cd4d689eb62ba51f458ae4ca94ea81 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Sun, 1 Dec 2024 14:22:07 -0500 Subject: [PATCH 12/15] Update how we validate output, allow for 1 second discrepancies in the result. --- .../DynamicNOAACalculatorTests.swift | 27 +++++++++++++++---- KosherCocoaTests/KosherCocoaTests.xctestplan | 3 ++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/KosherCocoaTests/DynamicNOAACalculatorTests.swift b/KosherCocoaTests/DynamicNOAACalculatorTests.swift index 652bfd5..6a51348 100644 --- a/KosherCocoaTests/DynamicNOAACalculatorTests.swift +++ b/KosherCocoaTests/DynamicNOAACalculatorTests.swift @@ -56,6 +56,7 @@ class DynamicNOAACalculatorTestCase : DynamicTestCase { /// - Returns: A dictionary of tests to invoke. override class func dynamicTests() -> [String : @convention(block) () -> Void]? { + let gregorian: Calendar = .init(identifier: .gregorian) var output: [String : @convention(block) () -> Void] = [:] /// Load the tests from a CSV file. @@ -71,7 +72,8 @@ class DynamicNOAACalculatorTestCase : DynamicTestCase { } /// Iterate the test cases, skipping the headers row. - for testCase in tests[1...] { + for testIdx in stride(from: 1, through: tests.count, by: 9) { + let testCase = tests[testIdx] /// The first field is the date. guard let inputDateAsString: String = testCase.first else { XCTFail("Missing date in test case: \(testCase)") @@ -95,7 +97,8 @@ class DynamicNOAACalculatorTestCase : DynamicTestCase { calendar.workingDate = workingDate - guard let performedSelector = calendar.perform(Selector(methodName)) else { + let selector = Selector(methodName) + guard let performedSelector = calendar.perform(selector) else { XCTFail("Failed to call calendar.perform(Selector())") return } @@ -118,14 +121,28 @@ class DynamicNOAACalculatorTestCase : DynamicTestCase { /// Further reading: /// https://developer.apple.com/forums/thread/731850 /// https://stackoverflow.com/questions/31272561/31483262#31483262 - guard let expected = DynamicNOAACalculatorTestCase.timeFormatter.date(from:testCase[methodIdx+1]) else { + guard let expectedTime = DynamicNOAACalculatorTestCase.timeFormatter.date(from:testCase[methodIdx+1]) else { XCTFail("Failed to parse date.") return } + /// Set the expected time on the input date - so we can compare the times on + /// the expected date. + let expectedComponents = gregorian.dateComponents([.hour, .minute, .second], from: expectedTime) + guard let expected = gregorian.date( + bySettingHour: expectedComponents.hour!, + minute: expectedComponents.minute!, + second: expectedComponents.second!, + of: workingDate + ) else { + XCTFail("Failed to construct expected result date.") + return + } + /// Compare the resulting points in time, allowing for a 1 second discrepancy. XCTAssertEqual( - Calendar.current.dateComponents([.hour, .minute, .second], from: result), - Calendar.current.dateComponents([.hour, .minute, .second], from: expected) + result.timeIntervalSinceReferenceDate, + expected.timeIntervalSinceReferenceDate, + accuracy: 1 ) } } diff --git a/KosherCocoaTests/KosherCocoaTests.xctestplan b/KosherCocoaTests/KosherCocoaTests.xctestplan index d8b0315..3cf4118 100644 --- a/KosherCocoaTests/KosherCocoaTests.xctestplan +++ b/KosherCocoaTests/KosherCocoaTests.xctestplan @@ -9,10 +9,11 @@ } ], "defaultOptions" : { - "codeCoverage" : false + }, "testTargets" : [ { + "parallelizable" : true, "target" : { "containerPath" : "container:KosherCocoa.xcodeproj", "identifier" : "46DB49631D9839F100F3A576", From 1c27402c05513e6ea27818a24383e4e7ec8023e2 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Sun, 1 Dec 2024 14:22:35 -0500 Subject: [PATCH 13/15] Update test scheme. --- .../xcshareddata/xcschemes/KosherCocoaTests.xcscheme | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/KosherCocoa.xcodeproj/xcshareddata/xcschemes/KosherCocoaTests.xcscheme b/KosherCocoa.xcodeproj/xcshareddata/xcschemes/KosherCocoaTests.xcscheme index c905ee2..1943fe4 100644 --- a/KosherCocoa.xcodeproj/xcshareddata/xcschemes/KosherCocoaTests.xcscheme +++ b/KosherCocoa.xcodeproj/xcshareddata/xcschemes/KosherCocoaTests.xcscheme @@ -1,7 +1,7 @@ + version = "1.7"> @@ -11,6 +11,12 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + From 486e4a95d0a43b4d9c042e5cf90dbe59f12a9ed2 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Sun, 1 Dec 2024 15:07:34 -0500 Subject: [PATCH 14/15] Add accuracy tolerance to manual tests. --- KosherCocoaTests/KCNOAACalculatorTests.swift | 56 +++++++++++++++----- 1 file changed, 44 insertions(+), 12 deletions(-) diff --git a/KosherCocoaTests/KCNOAACalculatorTests.swift b/KosherCocoaTests/KCNOAACalculatorTests.swift index 39e4d0b..96e1660 100644 --- a/KosherCocoaTests/KCNOAACalculatorTests.swift +++ b/KosherCocoaTests/KCNOAACalculatorTests.swift @@ -54,8 +54,12 @@ final class KCNOAACalculatorTests: XCTestCase { januaryFirst.minute = 18 januaryFirst.second = 57 - XCTAssertEqual(sunrise, gregorianCalendar.date(from: januaryFirst)) - + XCTAssertEqual( + sunrise!.timeIntervalSinceReferenceDate, + gregorianCalendar.date(from: januaryFirst)!.timeIntervalSinceReferenceDate, + accuracy: 1 + ) + var mayFirst = DateComponents() mayFirst.year = 2023 mayFirst.month = 5 @@ -68,8 +72,12 @@ final class KCNOAACalculatorTests: XCTestCase { mayFirst.minute = 56 mayFirst.second = 59 - XCTAssertEqual(sunrise, gregorianCalendar.date(from: mayFirst)) - + XCTAssertEqual( + sunrise!.timeIntervalSinceReferenceDate, + gregorianCalendar.date(from: mayFirst)!.timeIntervalSinceReferenceDate, + accuracy: 1 + ) + var augustFirst = DateComponents() augustFirst.year = 2023 augustFirst.month = 8 @@ -82,7 +90,11 @@ final class KCNOAACalculatorTests: XCTestCase { augustFirst.minute = 54 augustFirst.second = 51 - XCTAssertEqual(sunrise, gregorianCalendar.date(from: augustFirst)) + XCTAssertEqual( + sunrise!.timeIntervalSinceReferenceDate, + gregorianCalendar.date(from: augustFirst)!.timeIntervalSinceReferenceDate, + accuracy: 1 + ) var decFirst = DateComponents() decFirst.year = 2023 @@ -96,7 +108,11 @@ final class KCNOAACalculatorTests: XCTestCase { decFirst.minute = 59 decFirst.second = 29 - XCTAssertEqual(sunrise, gregorianCalendar.date(from: decFirst)) + XCTAssertEqual( + sunrise!.timeIntervalSinceReferenceDate, + gregorianCalendar.date(from: decFirst)!.timeIntervalSinceReferenceDate, + accuracy: 1 + ) } func testCalculatorSunset() throws { @@ -116,8 +132,12 @@ final class KCNOAACalculatorTests: XCTestCase { januaryFirst.minute = 41 januaryFirst.second = 56 - XCTAssertEqual(sunset, gregorianCalendar.date(from: januaryFirst)) - + XCTAssertEqual( + sunset!.timeIntervalSinceReferenceDate, + gregorianCalendar.date(from: januaryFirst)!.timeIntervalSinceReferenceDate, + accuracy: 1 + ) + var mayFirst = DateComponents() mayFirst.year = 2023 mayFirst.month = 5 @@ -130,8 +150,12 @@ final class KCNOAACalculatorTests: XCTestCase { mayFirst.minute = 51 mayFirst.second = 33 - XCTAssertEqual(sunset, gregorianCalendar.date(from: mayFirst)) - + XCTAssertEqual( + sunset!.timeIntervalSinceReferenceDate, + gregorianCalendar.date(from: mayFirst)!.timeIntervalSinceReferenceDate, + accuracy: 1 + ) + var augustFirst = DateComponents() augustFirst.year = 2023 augustFirst.month = 8 @@ -144,7 +168,11 @@ final class KCNOAACalculatorTests: XCTestCase { augustFirst.minute = 10 augustFirst.second = 56 - XCTAssertEqual(sunset, gregorianCalendar.date(from: augustFirst)) + XCTAssertEqual( + sunset!.timeIntervalSinceReferenceDate, + gregorianCalendar.date(from: augustFirst)!.timeIntervalSinceReferenceDate, + accuracy: 1 + ) var decFirst = DateComponents() decFirst.year = 2023 @@ -158,6 +186,10 @@ final class KCNOAACalculatorTests: XCTestCase { decFirst.minute = 31 decFirst.second = 56 - XCTAssertEqual(sunset, gregorianCalendar.date(from: decFirst)) + XCTAssertEqual( + sunset!.timeIntervalSinceReferenceDate, + gregorianCalendar.date(from: decFirst)!.timeIntervalSinceReferenceDate, + accuracy: 1 + ) } } From bc31b1ddbcdcc767faf8f5a99cfe1c2d8aa341c9 Mon Sep 17 00:00:00 2001 From: Moshe Berman Date: Sun, 1 Dec 2024 15:39:43 -0500 Subject: [PATCH 15/15] Update manual `KCNOAACalculatorTests` to use Lakewood NJ, and to define one method per test case. --- KosherCocoaTests/KCNOAACalculatorTests.swift | 123 ++++++++++--------- 1 file changed, 67 insertions(+), 56 deletions(-) diff --git a/KosherCocoaTests/KCNOAACalculatorTests.swift b/KosherCocoaTests/KCNOAACalculatorTests.swift index 96e1660..3067fe6 100644 --- a/KosherCocoaTests/KCNOAACalculatorTests.swift +++ b/KosherCocoaTests/KCNOAACalculatorTests.swift @@ -12,12 +12,15 @@ import XCTest import KosherCocoa final class KCNOAACalculatorTests: XCTestCase { - + static var timeZone: TimeZone = { + TimeZone(identifier: "America/New_York")! + }() let gregorianCalendar = Calendar(identifier: .gregorian) let lakewood: GeoLocation = GeoLocation( - latitude: 40.08213, - andLongitude: -74.20970, - andTimeZone: TimeZone(identifier: "America/New_York")! + latitude: 40.096, + andLongitude: -74.222, + elevation: 25.58, + andTimeZone: timeZone ) lazy var lakewoodCalculator:NOAACalculator = { NOAACalculator(geoLocation: lakewood) @@ -39,75 +42,79 @@ final class KCNOAACalculatorTests: XCTestCase { XCTAssertEqual(lakewoodCalculator.calculatorName, "US National Oceanic and Atmospheric Administration Algorithm") } - func testCalculatorSunrise() throws { - var januaryFirst = DateComponents() + func testCalculatesSunriseOnJanuaryFirstAsExpected() throws { + var januaryFirst = DateComponents(timeZone: KCNOAACalculatorTests.timeZone) januaryFirst.year = 2023 januaryFirst.month = 1 januaryFirst.day = 1 - - let calendar = AstronomicalCalendar(location: lakewoodCalculator.geoLocation) - calendar.astronomicalCalculator = lakewoodCalculator + calendar.workingDate = gregorianCalendar.date(from: januaryFirst)! var sunrise = calendar.sunrise() - + januaryFirst.hour = 7 januaryFirst.minute = 18 - januaryFirst.second = 57 - + januaryFirst.second = 03 + XCTAssertEqual( sunrise!.timeIntervalSinceReferenceDate, gregorianCalendar.date(from: januaryFirst)!.timeIntervalSinceReferenceDate, accuracy: 1 ) + } - var mayFirst = DateComponents() + func testCalculatesSunriseOnMayFirstAsExpected() throws { + var mayFirst = DateComponents(timeZone: KCNOAACalculatorTests.timeZone) mayFirst.year = 2023 mayFirst.month = 5 mayFirst.day = 1 - + calendar.workingDate = gregorianCalendar.date(from: mayFirst)! - sunrise = calendar.sunrise() - + var sunrise = calendar.sunrise() + mayFirst.hour = 5 mayFirst.minute = 56 - mayFirst.second = 59 - + mayFirst.second = 6 + XCTAssertEqual( sunrise!.timeIntervalSinceReferenceDate, gregorianCalendar.date(from: mayFirst)!.timeIntervalSinceReferenceDate, accuracy: 1 ) - + } + func testCalculatesSunriseOnAugustFirstAsExpected() throws { var augustFirst = DateComponents() augustFirst.year = 2023 augustFirst.month = 8 augustFirst.day = 1 - + calendar.workingDate = gregorianCalendar.date(from: augustFirst)! - sunrise = calendar.sunrise() - + var sunrise = calendar.sunrise() + augustFirst.hour = 5 - augustFirst.minute = 54 - augustFirst.second = 51 - + augustFirst.minute = 53 + augustFirst.second = 56 + XCTAssertEqual( sunrise!.timeIntervalSinceReferenceDate, gregorianCalendar.date(from: augustFirst)!.timeIntervalSinceReferenceDate, accuracy: 1 ) - + + } + + func testCalculatesSunriseOnDecemberFirstAsExpected() throws { var decFirst = DateComponents() decFirst.year = 2023 decFirst.month = 12 decFirst.day = 1 calendar.workingDate = gregorianCalendar.date(from: decFirst)! - sunrise = calendar.sunrise() - + var sunrise = calendar.sunrise() + decFirst.hour = 6 - decFirst.minute = 59 - decFirst.second = 29 - + decFirst.minute = 58 + decFirst.second = 36 + XCTAssertEqual( sunrise!.timeIntervalSinceReferenceDate, gregorianCalendar.date(from: decFirst)!.timeIntervalSinceReferenceDate, @@ -115,77 +122,81 @@ final class KCNOAACalculatorTests: XCTestCase { ) } - func testCalculatorSunset() throws { - let lakewoodCalculator = NOAACalculator(geoLocation: GeoLocation(latitude: 40.08213, andLongitude: -74.20970, andTimeZone: TimeZone(identifier: "America/New_York")!)) - + func testCalculatesSunsetOnJanuaryFirstAsExpected() throws { + var januaryFirst = DateComponents() januaryFirst.year = 2023 januaryFirst.month = 1 januaryFirst.day = 1 - - let calendar = AstronomicalCalendar(location: lakewoodCalculator.geoLocation) - calendar.astronomicalCalculator = lakewoodCalculator + calendar.workingDate = gregorianCalendar.date(from: januaryFirst)! var sunset = calendar.sunset() - + januaryFirst.hour = 16 - januaryFirst.minute = 41 + januaryFirst.minute = 42 januaryFirst.second = 56 - + XCTAssertEqual( sunset!.timeIntervalSinceReferenceDate, gregorianCalendar.date(from: januaryFirst)!.timeIntervalSinceReferenceDate, accuracy: 1 ) + } + func testCalculatesSunsetOnMayFirstAsExpected() throws { var mayFirst = DateComponents() mayFirst.year = 2023 mayFirst.month = 5 mayFirst.day = 1 - + calendar.workingDate = gregorianCalendar.date(from: mayFirst)! - sunset = calendar.sunset() - + var sunset = calendar.sunset() + mayFirst.hour = 19 - mayFirst.minute = 51 + mayFirst.minute = 52 mayFirst.second = 33 - + XCTAssertEqual( sunset!.timeIntervalSinceReferenceDate, gregorianCalendar.date(from: mayFirst)!.timeIntervalSinceReferenceDate, accuracy: 1 ) + } + func testCalculatesSunsetOnAugustFirstAsExpected() throws { var augustFirst = DateComponents() augustFirst.year = 2023 augustFirst.month = 8 augustFirst.day = 1 - + calendar.workingDate = gregorianCalendar.date(from: augustFirst)! - sunset = calendar.sunset() - + var sunset = calendar.sunset() + augustFirst.hour = 20 - augustFirst.minute = 10 - augustFirst.second = 56 - + augustFirst.minute = 11 + augustFirst.second = 58 + XCTAssertEqual( sunset!.timeIntervalSinceReferenceDate, gregorianCalendar.date(from: augustFirst)!.timeIntervalSinceReferenceDate, accuracy: 1 ) - + + } + + func testCalculatesSunsetOnDecemberFirstAsExpected() throws { var decFirst = DateComponents() decFirst.year = 2023 decFirst.month = 12 decFirst.day = 1 calendar.workingDate = gregorianCalendar.date(from: decFirst)! - sunset = calendar.sunset() + var sunset = calendar.sunset() decFirst.hour = 16 - decFirst.minute = 31 - decFirst.second = 56 - + decFirst.minute = 32 + decFirst.second = 55 + XCTAssertEqual( sunset!.timeIntervalSinceReferenceDate, gregorianCalendar.date(from: decFirst)!.timeIntervalSinceReferenceDate,