├── .swiftpm └── xcode │ ├── package.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── nmariniello.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ ├── seldon1000.xcuserdatad │ └── xcschemes │ │ └── xcschememanagement.plist │ └── nmariniello.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Resources ├── Settings.bundle │ ├── Root.plist │ ├── Acknowledgements.plist │ └── SunKit.plist └── sunkit.svg ├── .github └── workflows │ └── SunKit.yml ├── Package.swift ├── Sources └── SunKit │ ├── SunElevationEvents.swift │ ├── Angle.swift │ ├── HorizonCoordinates.swift │ ├── Extensions.swift │ ├── HMS.swift │ ├── EclipticCoordinates.swift │ ├── DMS.swift │ ├── EquatorialCoordinates.swift │ ├── Utils.swift │ └── Sun.swift ├── Tests └── SunKitTests │ ├── UT_HorizonCoordinates.swift │ ├── UT_EclipticCoordinates.swift │ ├── UT_HMS.swift │ ├── UT_DMS.swift │ ├── UT_Utils.swift │ ├── UT_EquatorialCoordinates.swift │ └── UT_Sun.swift ├── .gitignore ├── README.md └── LICENSE /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcuserdata/nmariniello.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SunKit-Swift/SunKit/HEAD/.swiftpm/xcode/package.xcworkspace/xcuserdata/nmariniello.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Resources/Settings.bundle/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | File 9 | Acknowledgements 10 | Title 11 | Acknowledgments 12 | Type 13 | PSChildPaneSpecifier 14 | 15 | 16 | StringsTable 17 | Root 18 | 19 | 20 | -------------------------------------------------------------------------------- /Resources/Settings.bundle/Acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | File 9 | Packages/SunKit 10 | Title 11 | SunKit 12 | Type 13 | PSChildPaneSpecifier 14 | 15 | 16 | StringsTable 17 | Acknowledgments 18 | 19 | 20 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/seldon1000.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SunKit.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | SunKit 16 | 17 | primary 18 | 19 | 20 | SunKitTests 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcuserdata/nmariniello.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | SunKit.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | SuppressBuildableAutocreation 14 | 15 | SunKit 16 | 17 | primary 18 | 19 | 20 | SunKitTests 21 | 22 | primary 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/workflows/SunKit.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Swift project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-swift 3 | 4 | name: Swift 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: macos-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: List available Xcode versions 21 | run: ls /Applications | grep Xcode 22 | - name: Set up Xcode version 23 | run: sudo xcode-select -s /Applications/Xcode_16.1.app/Contents/Developer 24 | - name: Build 25 | run: swift build -v 26 | - name: Run tests 27 | run: swift test -v 28 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SunKit", 8 | platforms: [ 9 | .iOS(.v14), .macOS(.v10_15), .tvOS(.v12), .watchOS(.v6) 10 | ], 11 | products: [ 12 | // Products define the executables and libraries a package produces, and make them visible to other packages. 13 | .library( 14 | name: "SunKit", 15 | targets: ["SunKit"]), 16 | ], 17 | dependencies: [ 18 | // Dependencies declare other packages that this package depends on. 19 | // .package(url: /* package url */, from: "1.0.0"), 20 | ], 21 | targets: [ 22 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 23 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 24 | .target( 25 | name: "SunKit", 26 | dependencies: []), 27 | .testTarget( 28 | name: "SunKitTests", 29 | dependencies: ["SunKit"]), 30 | ] 31 | ) 32 | -------------------------------------------------------------------------------- /Sources/SunKit/SunElevationEvents.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SunElevationEvents.swift 3 | // 4 | // 5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import Foundation 20 | 21 | enum SunElevationEvents: Double{ 22 | 23 | case civil = -6 24 | case nautical = -12 25 | case astronomical = -18 26 | case eveningGoldenHourStart = 6 27 | case eveningGoldenHourEnd = -4 28 | 29 | static var morningGoldenHourStart: SunElevationEvents { .eveningGoldenHourEnd } 30 | static var morningGoldenHourEnd: SunElevationEvents { .eveningGoldenHourStart } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SunKit/Angle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Angle.swift 3 | // 4 | // 5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | 20 | import Foundation 21 | 22 | public struct Angle: Equatable, Hashable, Codable, Sendable { 23 | 24 | public static var zero: Angle = .init() 25 | 26 | public init() { 27 | _radians = 0 28 | } 29 | 30 | public init(radians: Double) { 31 | _radians = radians 32 | } 33 | 34 | public init(degrees: Double) { 35 | _radians = degrees * Double.pi / 180.0 36 | } 37 | 38 | public var degrees: Double { 39 | get { _radians * 180.0 / Double.pi } 40 | set { _radians = newValue * Double.pi / 180 } 41 | } 42 | 43 | public var radians: Double { 44 | get { _radians } 45 | set { _radians = newValue } 46 | } 47 | 48 | private var _radians: Double 49 | 50 | public static func degrees(_ value: Double) -> Angle { 51 | .init(degrees: value) 52 | } 53 | 54 | public static func radians(_ value: Double) -> Angle { 55 | .init(radians: value) 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Sources/SunKit/HorizonCoordinates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HoorizonCoordinates.swift 3 | // 4 | // 5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import Foundation 20 | 21 | public struct HorizonCoordinates: Equatable, Hashable, Codable, Sendable { 22 | 23 | public var altitude: Angle 24 | public var azimuth: Angle 25 | 26 | /// Converts horizon coordinates to equatorial coordinates 27 | /// - Returns: Equatorial coordinates of the instance. 28 | public func horizon2Equatorial(latitude: Angle) -> EquatorialCoordinates{ 29 | 30 | let tZeroHorizonToEquatorial = sin(altitude.radians) * sin(latitude.radians) + cos(altitude.radians) * cos(latitude.radians) * cos(azimuth.radians) 31 | let declination: Angle = .init(radians:asin(tZeroHorizonToEquatorial)) 32 | 33 | let tOneHorizonToEquatorial = sin(altitude.radians) - sin(latitude.radians) * sin(declination.radians) 34 | 35 | let tTwoHorizonToEquatorial = tOneHorizonToEquatorial / (cos(latitude.radians) * cos(declination.radians)) 36 | 37 | var hourAngle: Angle = .init(radians: acos(tTwoHorizonToEquatorial)) 38 | 39 | if sin(altitude.radians) >= 0{ 40 | hourAngle.degrees = 360 - hourAngle.degrees 41 | } 42 | return .init(declination: declination, hourAngle: hourAngle) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/SunKitTests/UT_HorizonCoordinates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UT_HorizonCoordinates.swift 3 | // 4 | // 5 | // Copyright 2023 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import XCTest 20 | @testable import SunKit 21 | 22 | final class UT_HorizonCoordinates: XCTestCase { 23 | 24 | /// Test of horizon2Equatorial 25 | func testOfhorizon2Equatorial() throws { 26 | 27 | //Test1: Converting altitude 40° and azimuth 115° to equatorial coordinates for an observer at 38° N latitude. Expected out shall be hour angle = 21.031560h and declination = 8.084044°. 28 | 29 | //Step1: 30 | let horizonCoordinatesUnderTest: HorizonCoordinates = .init(altitude: .degrees(40), azimuth: .degrees(115)) 31 | 32 | //Step2: 33 | let equatorialCoordinates = horizonCoordinatesUnderTest.horizon2Equatorial(latitude: .init(degrees: 38)) 34 | 35 | //Step3: 36 | let expectedDeclination = 8.084044 37 | let expectedHourAngleDecimal = 21.031560 38 | 39 | //Step4: Converting hour angle in decimal 40 | let hourAngleDecimal = equatorialCoordinates.hourAngle!.degrees / 15 41 | 42 | //Step5: Check if declination and hour angle are closo to their respective expected outputs 43 | XCTAssertTrue(abs(equatorialCoordinates.declination.degrees - expectedDeclination) < 0.1) 44 | XCTAssertTrue(abs(hourAngleDecimal - expectedHourAngleDecimal) < 0.1) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Tests/SunKitTests/UT_EclipticCoordinates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UT_EclipticCoordinates.swift 3 | // 4 | // 5 | // Copyright 2023 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import XCTest 20 | @testable import SunKit 21 | import SwiftUI 22 | 23 | final class UT_EclipticCoordinates: XCTestCase { 24 | 25 | /// Test of ecliptic2Equatorial 26 | func testOfecliptic2Equatorial() throws { 27 | 28 | //Test1: Convert ecliptic coordinates with latitude = −3.956258 and longitude = 65.059853. Expected Equatorial coordinates are declination = 17.248880 and right ascension = 4.257714. 29 | 30 | //Step1: 31 | let eclipticCoordinatesUnderTest: EclipticCoordinates = .init(eclipticLatitude: .init(degrees: -3.956258), eclipticLongitude: .init(degrees: 65.059853)) 32 | 33 | //Step2: Saving expected values in output for both right ascension and declination 34 | let expectedRightAscension = 4.257714 35 | let expectedDeclination = 17.248880 36 | 37 | let rightAscension = eclipticCoordinatesUnderTest.ecliptic2Equatorial().rightAscension!.degrees 38 | let declination = eclipticCoordinatesUnderTest.ecliptic2Equatorial().declination.degrees 39 | 40 | //Step3: Check if output of the funciton under test is close to expected output for both right ascension and declination 41 | XCTAssertTrue(abs(rightAscension - expectedRightAscension) < 0.1) 42 | XCTAssertTrue(abs(declination - expectedDeclination) < 0.1) 43 | 44 | 45 | 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /Tests/SunKitTests/UT_HMS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UT_HMS.swift 3 | // 4 | // 5 | // Copyright 2023 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import XCTest 20 | @testable import SunKit 21 | 22 | final class UT_HMS: XCTestCase { 23 | 24 | /// Test of hMS2Decimal 25 | /// For 10h 25m 11s his decimal number should be 10.419722 26 | func testOfhMS2Decimal() throws { 27 | 28 | //Step1: Creating a HMS instance of 10h 25m 11s 29 | let hmsUnderTest: HMS = .init(hours: 10, minutes: 25, seconds: 11) 30 | let expectedOutput: Double = 10.419722 31 | 32 | //Step2: Call function under test and check that it returns a value close to expected output 33 | XCTAssertTrue(abs(hmsUnderTest.hMS2Decimal() - expectedOutput) < 0.01) 34 | } 35 | 36 | /// Test of HMSDecimalInit 37 | func testOfHMSDecimalInit() throws { 38 | 39 | 40 | //Test1: For 10.419722 decimal his HMS should be 10h 25m 11s 41 | 42 | //Step1: Creating a HMS instance of 10.419722 decimal number 43 | var hmsUnderTest: HMS = .init(decimal: 10.419722) 44 | 45 | //Step2: Call function under test and check that it returns a value close to expected output 46 | XCTAssertTrue((hmsUnderTest.hours == 10) && (hmsUnderTest.minutes == 25) && (10.9...11.1).contains(hmsUnderTest.seconds)) 47 | 48 | //Test2: For decimal equals 20.352 the DMS value should be equals to 20h 21m 7.2s 49 | 50 | //Step3: Creating a HMS instance of 10.419722 decimal number 51 | hmsUnderTest = .init(decimal: 20.352) 52 | 53 | //Step4: Call function under test and check that it returns a value close to expected output 54 | XCTAssertTrue((hmsUnderTest.hours == 20) && (hmsUnderTest.minutes == 21) && (7.1...7.3).contains(hmsUnderTest.seconds)) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/SunKit/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // 4 | // 5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import Foundation 20 | 21 | //It consents us too loop between two dates for n as interval time 22 | extension Date: @retroactive Strideable { 23 | public func distance(to other: Date) -> TimeInterval { 24 | return other.timeIntervalSinceReferenceDate - self.timeIntervalSinceReferenceDate 25 | } 26 | 27 | func toString(_ timeZone: TimeZone) -> String { 28 | let df = DateFormatter() 29 | df.timeZone = timeZone 30 | let custom = DateFormatter.dateFormat(fromTemplate: "MMdd HH:mm", 31 | options: 0, 32 | locale: Locale(identifier: "en")) 33 | df.dateFormat = custom 34 | return df.string(from: self) 35 | } 36 | 37 | public func advanced(by n: TimeInterval) -> Date { 38 | return self + n 39 | } 40 | } 41 | 42 | extension Calendar { 43 | func numberOfDaysSinceStartOfTheYear(for date: Date) -> Int { 44 | let startOfTheYear: Date = startOfYear(date) 45 | let startOfTheDay = startOfDay(for: date) 46 | let numberOfDays = dateComponents([.day], from: startOfTheYear, to: startOfTheDay) 47 | 48 | return numberOfDays.day! + 1 49 | } 50 | 51 | func startOfYear(_ date: Date) -> Date { 52 | return self.date(from: self.dateComponents([.year], from: date))! 53 | } 54 | 55 | } 56 | 57 | extension TimeZone { 58 | 59 | func offset(_ date: Date) -> Double { 60 | let res = 61 | Int(self.secondsFromGMT(for: date)) 62 | + Int(self.daylightSavingTimeOffset(for: date)) 63 | - Int(Calendar.current.timeZone.secondsFromGMT(for: date)) 64 | - Int(Calendar.current.timeZone.daylightSavingTimeOffset(for: date)) 65 | return Double(res)/SECONDS_IN_ONE_HOUR 66 | 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/SunKit/HMS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HMS.swift 3 | // 4 | // 5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import Foundation 20 | 21 | 22 | /// Time expressed in HMS format 23 | public struct HMS: Equatable, Hashable, Codable, Sendable { 24 | 25 | public var hours: Double 26 | public var minutes: Double 27 | public var seconds: Double 28 | 29 | public init(from date: Date){ 30 | 31 | var calendar: Calendar = .init(identifier: .gregorian) 32 | calendar.timeZone = .init(abbreviation: "GMT")! 33 | 34 | self.hours = Double(calendar.component(.hour, from: date)) 35 | self.minutes = Double(calendar.component(.minute, from: date)) 36 | self.seconds = Double(calendar.component(.second, from: date)) 37 | } 38 | 39 | public init(hours: Double,minutes: Double,seconds: Double){ 40 | 41 | self.hours = hours 42 | self.minutes = minutes 43 | self.seconds = seconds 44 | } 45 | 46 | public init(decimal: Double){ 47 | 48 | //Step1: 49 | let sign = decimal < 0 ? -1 : 1 50 | //Step2: 51 | let dec = abs(decimal) 52 | //Step3: 53 | var hours = Int(dec) 54 | //Step4: 55 | let minutes = Int(60 * dec.truncatingRemainder(dividingBy: 1)) 56 | //Step5: 57 | let seconds = 60 * (60 * dec.truncatingRemainder(dividingBy: 1)).truncatingRemainder(dividingBy: 1) 58 | //Step6: 59 | hours *= sign 60 | 61 | self.hours = Double(hours) 62 | self.minutes = Double(minutes) 63 | self.seconds = seconds 64 | 65 | } 66 | 67 | /// It converts from HMS format to decimal 68 | /// - Returns: HMS of the instance expressed in decimal format 69 | public func hMS2Decimal() -> Double { 70 | 71 | //Step3: 72 | let dm: Double = Double(seconds / 60) 73 | //Step4: 74 | let totalMinutes: Double = dm + Double(minutes) 75 | //Step5: 76 | var decimalHour: Double = totalMinutes / 60 77 | //Step6: 78 | decimalHour += Double(hours) 79 | 80 | return decimalHour 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Sources/SunKit/EclipticCoordinates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EclipticCoordinates.swift 3 | // 4 | // 5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import Foundation 20 | 21 | 22 | public struct EclipticCoordinates: Equatable, Hashable, Codable, Sendable { 23 | 24 | public static let obliquityOfTheEcliptic: Angle = .init(degrees: 23.439292) 25 | 26 | public var eclipticLatitude: Angle //beta 27 | public var eclipticLongitude: Angle //lambda 28 | 29 | /// Converts ecliptic coordinatates to equatorial coordinates 30 | /// - Returns: Equatorial coordinates of the instance 31 | public func ecliptic2Equatorial() -> EquatorialCoordinates{ 32 | 33 | //Step4: 34 | let tEclipticToEquatorial: Angle = .init(radians: sin(eclipticLatitude.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) + cos(eclipticLatitude.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians) * sin(eclipticLongitude.radians)) 35 | 36 | //Step5: 37 | let moonDeclination: Angle = .init(radians: asin(tEclipticToEquatorial.radians)) 38 | 39 | //Step6: 40 | let yEclipticToEquatorial = sin(eclipticLongitude.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) - tan(eclipticLatitude.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians) 41 | 42 | //Step7: 43 | let xEclipticToEquatorial = cos(eclipticLongitude.radians) 44 | 45 | //Step8: 46 | var r: Angle = .init(radians: atan(yEclipticToEquatorial / xEclipticToEquatorial)) 47 | 48 | //Step9: 49 | switch (yEclipticToEquatorial >= 0,xEclipticToEquatorial >= 0){ 50 | 51 | case (true, true): 52 | break 53 | case (true,false): 54 | r.degrees += 180 55 | case(false,true): 56 | r.degrees += 360 57 | case(false,false): 58 | r.degrees += 180 59 | } 60 | 61 | let alfa: Angle = .init(degrees: r.degrees / 15) 62 | let delta: Angle = .init(degrees: moonDeclination.degrees) 63 | 64 | return .init(declination: delta, rightAscension: alfa) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Resources/sunkit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Sources/SunKit/DMS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DMS.swift 3 | // 4 | // 5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import Foundation 20 | 21 | /// DMS format to express angles 22 | public struct DMS: Equatable, Hashable, Codable, Sendable { 23 | 24 | public var degrees: Double 25 | public var minutes: Double 26 | public var seconds: Double 27 | public var isANegativeZero: Bool 28 | 29 | 30 | init(degrees: Double, minutes: Double, seconds: Double, isANegativeZero: Bool = false) { 31 | self.degrees = degrees 32 | self.minutes = minutes 33 | self.seconds = seconds 34 | self.isANegativeZero = isANegativeZero 35 | } 36 | 37 | ///From decimal it will create the corresponding angle in DMS format 38 | /// - Parameter decimal: Decimal angle that will be converted in DMS format 39 | public init(decimal: Double){ 40 | 41 | //Step1: 42 | let sign = decimal < 0 ? -1 : 1 43 | //Step2: 44 | let dec = abs(decimal) 45 | //Step3: 46 | var degrees = Int(dec) 47 | //Step4: 48 | let minutes = Int(60 * dec.truncatingRemainder(dividingBy: 1)) 49 | //Step5: 50 | let seconds = 60 * (60 * dec.truncatingRemainder(dividingBy: 1)).truncatingRemainder(dividingBy: 1) 51 | //Step6: 52 | degrees *= sign 53 | if degrees == 0 && sign == -1 { 54 | self.degrees = Double(degrees) 55 | self.minutes = Double(minutes) 56 | self.seconds = seconds 57 | self.isANegativeZero = true 58 | } 59 | else{ 60 | self.degrees = Double(degrees) 61 | self.minutes = Double(minutes) 62 | self.seconds = seconds 63 | self.isANegativeZero = false 64 | } 65 | 66 | 67 | } 68 | 69 | /// It converts from DMS format to decimal 70 | /// - Returns: DMS of the instance expressed in decimal format 71 | public func dMS2Decimal() -> Double { 72 | 73 | //Step1: 74 | let sign: Double = degrees < 0 ? -1 : 1 75 | //Step2: 76 | let degrees = abs(degrees) 77 | //Step3: 78 | let dm: Double = Double(seconds / 60) 79 | //Step4: 80 | let totalMinutes: Double = dm + Double(minutes) 81 | //Step5: 82 | var decimal: Double = totalMinutes / 60 83 | //Step6: 84 | decimal += Double(degrees) 85 | //Step7: 86 | decimal *= sign 87 | 88 | if isANegativeZero{ 89 | 90 | decimal *= -1 91 | } 92 | 93 | return decimal 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/SunKitTests/UT_DMS.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UT_DMS.swift 3 | // 4 | // 5 | // Copyright 2023 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import XCTest 20 | @testable import SunKit 21 | 22 | final class UT_DMS: XCTestCase { 23 | 24 | 25 | /// Test of DMS init with decimal in input 26 | func testOfDMSDecimailInit(){ 27 | 28 | //Test1: Convert −0.508333° to DMS format. The result shall be -0°30''30'. 29 | 30 | //Step1: Initialize dmsUnderTest to −0.508333° and saving the expected output 31 | var dmsUnderTest: DMS = .init(decimal: -0.508333) 32 | 33 | //Step2: Check if the the values set is inside the correct range and isANegativeZero is TRUE 34 | XCTAssertTrue((dmsUnderTest.degrees == 0) && (dmsUnderTest.minutes == 30) && (29.99...30).contains(dmsUnderTest.seconds) && dmsUnderTest.isANegativeZero) 35 | 36 | //Test2: Convert 0.508333° to DMS format. The result shall be 0°30''30'. 37 | 38 | //Step3: Initialize dmsUnderTest to 0.508333° and saving the expected output 39 | dmsUnderTest = .init(decimal: 0.508333) 40 | 41 | //Step4: Check if the the values set is inside the correct range and isANegativeZero is FAlse 42 | XCTAssertTrue((dmsUnderTest.degrees == 0) && (dmsUnderTest.minutes == 30) && (29.99...30).contains(dmsUnderTest.seconds) && !dmsUnderTest.isANegativeZero) 43 | 44 | //Test3: Convert -300.3333° to DMS format. The result shall be -300° 20′ 00′′. 45 | 46 | //Step5: Creating a DMS instance of and saving the expected output 47 | dmsUnderTest = .init(decimal: -300.3333) 48 | 49 | //Step6: Call function under test and check that it returns a value close to expected output 50 | XCTAssertTrue((dmsUnderTest.degrees == -300) && (19...20).contains(dmsUnderTest.minutes) && (59...59.99).contains(dmsUnderTest.seconds) && !dmsUnderTest.isANegativeZero) 51 | 52 | } 53 | 54 | 55 | /// Test of dMS2Decimal 56 | func testOfdMS2Decimal(){ 57 | 58 | //Test1: For 300° 20′ 00′′ his decimal number should be 300.3333 59 | //Step1: Creating a DMS instance of 300° 20′ 00′′ and saving the expected output 60 | var dmsUnderTest: DMS = .init(degrees: 300, minutes: 20, seconds: 00) 61 | var expectedOutput: Double = 300.3333 62 | 63 | //Step2: Call function under test and check that it returns a value close to expected output 64 | XCTAssertTrue(abs(dmsUnderTest.dMS2Decimal() - expectedOutput) < 0.01) 65 | 66 | //Test2: For -0°30''30' his decimal number should be -0.508333 67 | 68 | //Step3: Creating a DMS instance of -0°30''30' and saving the expected output 69 | dmsUnderTest = .init(degrees: 0, minutes: 30, seconds: 30,isANegativeZero: true) 70 | expectedOutput = -0.508333 71 | 72 | //Step4: Call function under test and check that it returns a value close to expected output 73 | XCTAssertTrue(abs(dmsUnderTest.dMS2Decimal() - expectedOutput) < 0.01) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/SunKitTests/UT_Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UT_Utils.swift 3 | // 4 | // 5 | // Copyright 2023 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import XCTest 20 | @testable import SunKit 21 | 22 | 23 | final class UT_Utils: XCTestCase { 24 | 25 | /// Test of mod 26 | func testOfmod() throws { 27 | 28 | //Test1: -100 % 4 shall be equal to 4 29 | //Step1: call function under test and check that it returns 4 30 | var a = -100 31 | var n = 8 32 | XCTAssertTrue(4 == mod(a, n)) 33 | //Test2: -400 % 360 shall be equal to 320 34 | //Step1: call function under test and check that it returns 320 35 | a = -400 36 | n = 360 37 | XCTAssertTrue(320 == mod(a, n)) 38 | //Test3: 270 % 180 shall be equal to 90 39 | //Step1: call function under test and check that it returns 90 40 | a = 270 41 | n = 180 42 | XCTAssertTrue(90 == mod(a, n)) 43 | } 44 | 45 | /// Test of jdFromDate 46 | func testOfjdFromDate() throws{ 47 | 48 | //Test1: For 5/6/2010 at noon UT, his JD number shall be 2455323.0 49 | //Step1: Creating UTC date 50 | let dateUnderTest = createDateUTC(day: 6, month: 5, year: 2010, hour: 12, minute: 0, seconds: 0) 51 | 52 | //Step2:Call function under test, check that it returns expected output 53 | XCTAssertTrue(2455323.0 == jdFromDate(date: dateUnderTest)) 54 | } 55 | 56 | /// Test of dateFromJD 57 | func testOfdateFromJD() throws{ 58 | 59 | //Test1: 2455323.0 Jd number shall be quals to date 5/6/2010 at noon UT 60 | //Step1: Creating jd number under test 61 | let jdNumberTest = 2455323.0 62 | 63 | //Step2:Call function under test, check that it returns expected output 64 | let expectedOutput = createDateUTC(day: 6, month: 5, year: 2010, hour: 12, minute: 0, seconds: 0) 65 | XCTAssertTrue(expectedOutput == dateFromJd(jd: jdNumberTest)) 66 | } 67 | 68 | /// Test of extendMod 69 | func testOfExtendedMod() throws { 70 | 71 | //Test4: -270.8 % 180 shall be equal to 89.2 72 | var a: Double = -270.8 73 | var n = 180 74 | 75 | XCTAssertTrue(abs(89.2 - extendedMod(a,n)) < 0.1) 76 | 77 | //Test2: -0.8 % 1 shall be equal to 0.2 78 | a = -0.8 79 | n = 1 80 | 81 | XCTAssertTrue(abs(0.2 - extendedMod(a,n)) < 0.1) 82 | 83 | //Test3: 390.5 % 360 shall be equal to 30.5 84 | a = 390.5 85 | n = 360 86 | 87 | XCTAssertTrue(30.5 == extendedMod(a,n)) 88 | 89 | //Test4: 0.3 % 1 shall be equal to 0.3 90 | a = 0.3 91 | n = 1 92 | 93 | XCTAssertTrue(0.3 == extendedMod(a,n)) 94 | 95 | //Test1: -100 % 4 shall be equal to 4 96 | //Step1: call function under test and check that it returns 4 97 | a = -100 98 | n = 8 99 | 100 | XCTAssertTrue(4 == extendedMod(a, n)) 101 | //Test2: -400 % 360 shall be equal to 320 102 | //Step1: call function under test and check that it returns 320 103 | a = -400 104 | n = 360 105 | XCTAssertTrue(320 == extendedMod(a, n)) 106 | //Test3: 270 % 180 shall be equal to 90 107 | //Step1: call function under test and check that it returns 90 108 | a = 270 109 | n = 180 110 | XCTAssertTrue(90 == extendedMod(a, n)) 111 | } 112 | 113 | /// Test of uT2GST 114 | func testOfuT2GST() throws{ 115 | 116 | //Test2: Convert 23h30m00s UT to GST for February 7, 2010. UseSameTimeZone equals False. 117 | 118 | //Step1: Creating 7/02/2010 23h30m UTC 119 | let dateUnderTest = createDateUTC(day: 7, month: 2, year: 2010, hour: 23, minute: 30, seconds: 0) 120 | 121 | //Step2: Set variable "expectedOutput" to the expect output 122 | let expectedOutput: HMS = HMS.init(decimal: 8.698091) 123 | 124 | //Step3: Call function under test and check that it returns a value which differs from expected output up to 0.01 125 | XCTAssert(abs(uT2GST(dateUnderTest).hMS2Decimal() - expectedOutput.hMS2Decimal()) < 0.01) 126 | } 127 | 128 | /// Test of gST2LST 129 | func testOfgST2LST() throws{ 130 | 131 | //Test1: Converting GST to LST requires knowing an observer’s longitude. Assume that the GST is 2h03m41s for an observer at 40° W longitude. 132 | 133 | //Step1: Creating 7/02/2010 2h03m41s with current time zone(i.e the one set on your device) 134 | let gstHMS: HMS = .init(hours: 2, minutes: 03, seconds: 41) 135 | let longitudeUnderTest: Angle = .init(degrees: -40) 136 | 137 | //Step2: Set variable "expectedOutput" to the expected output 138 | let expectedOutput = 23.3994722 139 | 140 | //Step3: Call function under test and check that it returns a value which differs from expected output up to 0.01 141 | let output = gST2LST(gstHMS, longitude: longitudeUnderTest).hMS2Decimal() 142 | XCTAssert(abs(expectedOutput - output) < 0.01) 143 | 144 | } 145 | 146 | 147 | 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SunKit 2 | 3 |
4 | 5 | sunkit 6 | 7 | ![GitHub](https://img.shields.io/github/license/sunlitt/sunkit) [![GitHub stars](https://img.shields.io/github/stars/Sunlitt/SunKit)](https://github.com/Sunlitt/SunKit/stargazers) [![GitHub issues](https://img.shields.io/github/issues/Sunlitt/SunKit)](https://github.com/Sunlitt/SunKit/issues) [![Requires Core Location](https://img.shields.io/badge/requires-CoreLocation-orange?style=flat&logo=Swift)](https://developer.apple.com/documentation/corelocation) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSunlitt%2FSunKit%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/Sunlitt/SunKit) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FSunlitt%2FSunKit%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/Sunlitt/SunKit) 8 | 9 |
10 | 11 | **SunKit** is a Swift package that uses math and trigonometry to compute several pieces of information about the Sun. 12 | 13 | SunKit was first developed as part of a bigger project: [Sunlitt](https://github.com/Sunlitt/Sunlitt-AppStore). Even though Sunlitt is not meant to be open-sourced, we decided to wrap the fundamental logic of the app and build a library out of it, free for anyone to use, embrace and extend. 14 | 15 | sunkit [](https://apps.apple.com/app/sunlitt/id1628751457) 16 | 17 | ## Attribution 18 | SunKit is licensed under the Apache License 2.0. For attribution, we request that you include an unmodified vector of SunKit's logo, available [here](Resources/sunkit.svg), along with our organization name "Sunlitt", both at legible sizes, and that they link back to our [website](https://sunlitt.app). 19 | 20 | If you are developing an app for Apple platforms, we additionally request that you include SunKit's license and copyright in the "Acknowledgments" section of your app's Settings.bundle file. We have included a Settings.bundle example [here](Resources/Settings.bundle) for you to download and import as is in your Xcode project. 21 | 22 | Attribution is essential to make sure that our hard work is properly recognized and we thank you for complying with our requests. 23 | 24 | 25 | ## Usage 26 | **CoreLocation framework is required for SunKit to work**. SunKit only needs a location and the relative time zone. Everything is computed locally, no internet connection is needed. 27 | 28 | ### Creating a Sun 29 | 30 | ```swift 31 | 32 | // Creating a CLLocation object with the coordinates you are interested in 33 | let naplesLocation: CLLocation = .init(latitude: 40.84014, longitude: 14.25226) 34 | 35 | // Timezone for the location of interest. It's highly recommended to initialize it via identifier 36 | let timeZoneNaples: Timezone = .init(identifier: "Europe/Rome") ?? .current 37 | 38 | // Creating the Sun instance which will store all the information you need about sun events and his position 39 | var mySun: Sun = .init(location: naplesLocation, timeZone: timeZoneNaples) 40 | 41 | ``` 42 | 43 | ### Retrieve information 44 | 45 | ```swift 46 | // Creating a Date instance 47 | let myDate: Date = Date() // Your current date 48 | 49 | // Setting inside mySun object the date of interest 50 | mySun.setDate(myDate) 51 | 52 | // All the following informations are related to the given location for the date that has just been set 53 | 54 | // Azimuth of the Sun 55 | mySun.azimuth.degrees 56 | 57 | // Altitude of the Sun 58 | mySun.altitude.degrees 59 | 60 | // Sunrise Date 61 | mySun.sunrise 62 | 63 | // Sunset Date 64 | mySun.sunset 65 | 66 | // Evening Golden Hour Start Date 67 | mySun.eveningGoldenHourStart 68 | 69 | // Evening Golden Hour End Date 70 | mySun.eveningGoldenHourEnd 71 | 72 | // To know all the information you can retrieve go to the **Features** section. 73 | 74 | ``` 75 | 76 | 77 | ### Working with Timezones and Dates 78 | To properly show the Sun Date Events use the following DateFormatter. 79 | 80 | ```swift 81 | 82 | //Creting a DateFormatter 83 | let dateFormatter = DateFormatter() 84 | 85 | //Properly setting his attributes 86 | dateFormatter.locale = .current 87 | dateFormatter.timeZone = timeZoneNaples // It shall be the same as the one used to initilize mySun 88 | dateFormatter.timeStyle = .full 89 | dateFormatter.dateStyle = .full 90 | 91 | //Printing Sun Date Events with the correct Timezone 92 | 93 | print("Sunrise: \(dateFormatter.string(from: mySun.sunrise))") 94 | 95 | ``` 96 | 97 | ## Features 98 | * Sun Azimuth 99 | * Sun Altitude 100 | * Civil Dusk Time 101 | * Civil Dawn Time 102 | * Sunrise Time 103 | * Solar Noon Time 104 | * Solar Midnight Time 105 | * Morning Golden Hour Time 106 | * Evening Golden Hour Time 107 | * Sunset Time 108 | * Astronomical Dusk 109 | * Astronomical Dawn 110 | * Nautical Dusk 111 | * Nautical Dawn 112 | * Morning Blue Hour Time 113 | * Evening Blue Hour Time 114 | * Sun Azimuth at Sunrise 115 | * Sun Azimuth at Sunset 116 | * Sun Azimuth at Solar Noon 117 | * Total Daylight Duration 118 | * Total Night Duration 119 | * March Equinox 120 | * June Solstice 121 | * September Equinox 122 | * December Solstice 123 | 124 | 125 | ## References 126 | 127 | * NOAA Global Monitoring Division. General Solar Position Calculations: [Link](https://gml.noaa.gov/grad/solcalc/solareqns.PDF). 128 | * PV Education: [Link](https://www.pveducation.org). 129 | * Celestial Calculations: A Gentle Introduction to Computational Astronomy: [Link](https://www.amazon.it/Celestial-Calculations-Introduction-Computational-Astronomy/dp/0262536633/ref=sr_1_1?__mk_it_IT=ÅMÅŽÕÑ&crid=1U99GMGDZ2CUF&keywords=celestial+calculations&qid=1674408445&sprefix=celestial+calculation%2Caps%2C109&sr=8-1). 130 | 131 | ## MoonKit 🌙 132 | Take a look at SunKit's spiritual brother, [MoonKit](https://github.com/davideilmito/MoonKit). 133 | 134 | ## Special thanks 135 | * [Davide Biancardi](https://github.com/davideilmito): Creator of SunKit. 136 | -------------------------------------------------------------------------------- /Tests/SunKitTests/UT_EquatorialCoordinates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UT_EquatorialCoordinates.swift 3 | // 4 | // 5 | // Copyright 2023 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import XCTest 20 | @testable import SunKit 21 | 22 | final class UT_EquatorialCoordinates: XCTestCase { 23 | 24 | 25 | /// Test of EquatorialCoordinates init 26 | func testOfInitEquatorialCoordinates() throws { 27 | 28 | //Test1: Consider a star whose right ascension is 3h24m06s and declination = −0°30'30''. Suppose the LST for an observer is 18h.Calculate the corresponding hour angle. 29 | 30 | //Step1: 31 | let declinationUnderTest: Angle = .init(degrees: DMS.init(degrees:0 , minutes: 30, seconds: 30,isANegativeZero: true).dMS2Decimal()) 32 | let rightAscensionUnderTest: Angle = .init(degrees: HMS.init(hours: 3, minutes: 24, seconds: 06).hMS2Decimal()) 33 | var equatorialCoordinatesUnderTest: EquatorialCoordinates = .init(declination: declinationUnderTest, rightAscension: rightAscensionUnderTest) 34 | //Step2: Converting hour Angle from angle to decimal 35 | let hourAngleDecimal = equatorialCoordinatesUnderTest.setHourAngleFrom(lstDecimal: 18)!.degrees / 15 36 | 37 | //Step3: 38 | XCTAssertTrue(abs(hourAngleDecimal - 14.598333) < 0.1) 39 | 40 | } 41 | 42 | 43 | /// Test of equatorial2Horizon 44 | func testOfequatorial2Horizon() throws { 45 | 46 | 47 | //Test1: Convert equatorial coordinates with declination = 17.248880 and right ascension = 4.257714. Expected horizon coordinates are altitude = 68°52 and Azimuth = 192°11′. For an observer at 38° N and LST is 4.562547h 48 | 49 | //Step1: 50 | var equatorialCoordinatesUnderTest: EquatorialCoordinates = .init(declination: .init(degrees: 17.248880), rightAscension: .init(degrees: 4.257714)) 51 | var latitudeUnderTest: Angle = .init(degrees: 38) 52 | let lstUnderTest = 4.562547 53 | 54 | //Step2: Saving expected values in output for both azimuth and altitude 55 | var expectedAltitude = DMS.init(degrees: 68, minutes: 52, seconds: 0).dMS2Decimal() 56 | var expectedAzimuth = DMS.init(degrees: 192, minutes: 11, seconds: 0).dMS2Decimal() 57 | 58 | var azimuth = equatorialCoordinatesUnderTest.equatorial2Horizon(lstDecimal: lstUnderTest, latitude: latitudeUnderTest)!.azimuth.degrees 59 | var altitude = equatorialCoordinatesUnderTest.equatorial2Horizon(lstDecimal: lstUnderTest, latitude: latitudeUnderTest)!.altitude.degrees 60 | 61 | //Step3: Check if output of the function under test is close to expected output for both azimuth and altitude 62 | XCTAssertTrue(abs(azimuth - expectedAzimuth) < 0.1) 63 | XCTAssertTrue(abs(altitude - expectedAltitude) < 0.1) 64 | 65 | 66 | //Test2: Suppose a star is located at δ = −0°30′30′′, H = 16h29m45s. For an observer at 25° N latitude. Expected output shall be Azimuth = 80°31′31′′ ,and −20°34′40′′. 67 | 68 | //Step4: 69 | let hourAngleDecimal = HMS.init(hours: 16, minutes: 29, seconds: 45).hMS2Decimal() 70 | let hourAngle: Angle = .init(degrees: hourAngleDecimal * 15) 71 | latitudeUnderTest = .degrees(25) 72 | let declinationUnderTest: Angle = .init(degrees: DMS.init(degrees: 0, minutes: 30, seconds: 30,isANegativeZero: true).dMS2Decimal()) 73 | 74 | //Step5: Creating a new instance of EquatorialCoordinates initialized with declinaiton and hour angle under test 75 | equatorialCoordinatesUnderTest = .init(declination: declinationUnderTest, hourAngle: hourAngle) 76 | 77 | //Step6: Saving expected values in output for both azimuth and altitude 78 | expectedAltitude = DMS.init(degrees: -20, minutes: 34, seconds: 40).dMS2Decimal() 79 | expectedAzimuth = DMS.init(degrees: 80, minutes: 31, seconds: 31).dMS2Decimal() 80 | 81 | azimuth = equatorialCoordinatesUnderTest.equatorial2Horizon(latitude: latitudeUnderTest)!.azimuth.degrees 82 | altitude = equatorialCoordinatesUnderTest.equatorial2Horizon(latitude: latitudeUnderTest)!.altitude.degrees 83 | 84 | //Step7: Check if output of the function under test is close to expected output for both azimuth and altitude 85 | XCTAssertTrue(abs(azimuth - expectedAzimuth) < 0.1) 86 | XCTAssertTrue(abs(altitude - expectedAltitude) < 0.1) 87 | } 88 | 89 | /// Test of equatorial2Ecliptic 90 | func testOfequatorial2Ecliptic() throws { 91 | 92 | //Test1: Given Jupiter’s equatorial coordinates of right ascension 12h18m47.5s, declination −0°43′35.5'', and the standard epoch J2000, compute Jupiter’s ecliptic coordinates.Expected output shall be eclipitc latitude = 1°12′00.0′′ and ecliptic longitude = 184°36′00.0′′ 93 | 94 | //Step1: 95 | let rightAscensionUnderTest = HMS.init(hours: 12, minutes: 18, seconds: 47.5).hMS2Decimal() 96 | let declinationUnderTest = DMS.init(degrees: 0, minutes: 43, seconds: 35.5,isANegativeZero: true).dMS2Decimal() 97 | let equatorialCoordinatesUnderTest: EquatorialCoordinates = .init(declination: .degrees(declinationUnderTest), rightAscension: .degrees(rightAscensionUnderTest)) 98 | 99 | let eclipticCoordinates = equatorialCoordinatesUnderTest.equatorial2Ecliptic() ?? .init(eclipticLatitude: .zero, eclipticLongitude: .zero) 100 | 101 | //Step2: Saving expected values in output for both latitude and longitude 102 | let expectedLatitude = DMS.init(degrees: 1, minutes: 12, seconds: 0).dMS2Decimal() 103 | let expectedLongitude = DMS.init(degrees: 184, minutes: 36, seconds: 0).dMS2Decimal() 104 | 105 | //Step3: Check if output of the function under test is close to expected output for both latitude and longitude 106 | XCTAssertTrue(abs(expectedLatitude - eclipticCoordinates.eclipticLatitude.degrees) < 0.1) 107 | XCTAssertTrue(abs(expectedLongitude - eclipticCoordinates.eclipticLongitude.degrees) < 0.1) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/SunKit/EquatorialCoordinates.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EquatorialCoordinates.swift 3 | // 4 | // 5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import Foundation 20 | 21 | public struct EquatorialCoordinates: Equatable, Hashable, Codable, Sendable { 22 | 23 | public private(set) var rightAscension: Angle? // rightAscension.degrees refers to h format 24 | public private(set) var declination: Angle //delta 25 | 26 | private(set) var hourAngle: Angle? 27 | 28 | init(declination: Angle,rightAscension: Angle, hourAngle: Angle){ 29 | self.declination = declination 30 | self.rightAscension = rightAscension 31 | self.hourAngle = hourAngle 32 | } 33 | 34 | init(declination: Angle,rightAscension: Angle){ 35 | self.declination = declination 36 | self.rightAscension = rightAscension 37 | self.hourAngle = nil 38 | } 39 | 40 | init(declination: Angle,hourAngle: Angle){ 41 | self.declination = declination 42 | self.hourAngle = hourAngle 43 | self.rightAscension = nil 44 | } 45 | 46 | init(declination: Angle){ 47 | self.declination = declination 48 | } 49 | 50 | /// To set right ascension we need LST and right ascension. If right ascension is nill we can't set it. 51 | /// - Parameter lstDecimal: Local Sideral Time in decimal 52 | /// - Returns: The value of hour angle just been set. Nil if right ascension is also nil 53 | public mutating func setHourAngleFrom(lstDecimal: Double) -> Angle?{ 54 | 55 | guard let rightAscension = self.rightAscension else {return nil} 56 | 57 | var hourAngleDecimal = lstDecimal - rightAscension.degrees 58 | if hourAngleDecimal < 0 { 59 | hourAngleDecimal += 24 60 | } 61 | self.hourAngle = .init(degrees: hourAngleDecimal * 15) 62 | 63 | return self.hourAngle 64 | } 65 | 66 | /// To set right ascension we need LST and hour angle. If hour angle is nill we can't set it. 67 | /// - Parameter lstDecimal: Local Sideral Time in decimal 68 | /// - Returns: The value of right ascension just been set. Nil if hour angle is also nil 69 | public mutating func setRightAscensionFrom(lstDecimal: Double) -> Angle?{ 70 | 71 | guard let hourAngle = self.hourAngle else {return nil} 72 | 73 | let hourAngleDecimal = hourAngle.degrees / 15 74 | 75 | self.rightAscension = .init(degrees: lstDecimal - hourAngleDecimal) 76 | 77 | return self.rightAscension 78 | } 79 | 80 | 81 | /// Converts Equatorial coordinates to Horizon coordinates. 82 | /// 83 | /// Since horizon coordinates depend on the position, we need also latitude parameter to create an EquatorialCoordinates instance. 84 | /// 85 | /// - Parameters: 86 | /// - lstDecimal: Local Sidereal Time in decimal format. 87 | /// - latitude: Latitude of the observer 88 | /// - Returns: Horizon coordinates for the given latitude and LST. Nil if hour angle cannot be computed due to the miss right ascnsion information 89 | public mutating func equatorial2Horizon(lstDecimal: Double,latitude: Angle) -> HorizonCoordinates?{ 90 | 91 | guard let _ = setHourAngleFrom(lstDecimal: lstDecimal) else {return nil} 92 | 93 | //Step4: 94 | let tZeroEquatorialToHorizon = sin(declination.radians) * sin(latitude.radians) + cos(declination.radians) * cos(latitude.radians) * cos(hourAngle!.radians) 95 | 96 | //Step5: 97 | let altitude: Angle = .init(radians: asin(tZeroEquatorialToHorizon)) 98 | 99 | //Step6: 100 | let tOneEquatorialToHorizon = sin(declination.radians) - sin(latitude.radians) * sin(altitude.radians) 101 | 102 | //Step7: 103 | let tTwoEquatorialToHorizon = tOneEquatorialToHorizon / (cos(latitude.radians) * cos(altitude.radians)) 104 | 105 | //Step8: 106 | var azimuth: Angle = .init(radians: acos(tTwoEquatorialToHorizon)) 107 | if sin(hourAngle!.radians) >= 0{ 108 | azimuth.degrees = 360 - azimuth.degrees 109 | } 110 | 111 | return .init(altitude: altitude, azimuth: azimuth) 112 | } 113 | 114 | /// Converts Equatorial coordinates to Horizon coordinates. 115 | /// 116 | /// Since horizon coordinates depend on the position, we need also latitude parameter to create an EquatorialCoordinates instance. 117 | /// 118 | /// - Parameters: 119 | /// - latitude: Latitude of the observer 120 | /// - Returns: Horizon coordinates for the given latitude. Nil if hour angle is not defined. 121 | public func equatorial2Horizon(latitude: Angle) -> HorizonCoordinates?{ 122 | 123 | guard let _ = self.hourAngle else {return nil} 124 | 125 | //Step4: 126 | let tZeroEquatorialToHorizon = sin(declination.radians) * sin(latitude.radians) + cos(declination.radians) * cos(latitude.radians) * cos(hourAngle!.radians) 127 | 128 | //Step5: 129 | let altitude: Angle = .init(radians: asin(tZeroEquatorialToHorizon)) 130 | 131 | //Step6: 132 | let tOneEquatorialToHorizon = sin(declination.radians) - sin(latitude.radians) * sin(altitude.radians) 133 | 134 | //Step7: 135 | let tTwoEquatorialToHorizon = tOneEquatorialToHorizon / (cos(latitude.radians) * cos(altitude.radians)) 136 | 137 | //Step8: 138 | var azimuth: Angle = .init(radians: acos(tTwoEquatorialToHorizon)) 139 | if sin(hourAngle!.radians) >= 0{ 140 | azimuth.degrees = 360 - azimuth.degrees 141 | } 142 | 143 | return .init(altitude: altitude, azimuth: azimuth) 144 | } 145 | 146 | public func equatorial2Ecliptic() -> EclipticCoordinates?{ 147 | 148 | guard var rightAscension = rightAscension else {return nil} 149 | 150 | rightAscension.degrees = rightAscension.degrees * 15 //from h format to degrees 151 | 152 | //Step5: 153 | let tEquatorialToEcliptic: Angle = .init(radians: sin(declination.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) - cos(declination.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians) * sin(rightAscension.radians)) 154 | 155 | //Step6: 156 | let eclipticLatitude: Angle = .init(radians: asin(tEquatorialToEcliptic.radians)) 157 | 158 | //Step7: 159 | let yEquatorialToEcliptic = sin(rightAscension.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) + tan(declination.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians) 160 | 161 | //Step8: 162 | let xEquatorialToEcliptic = cos(rightAscension.radians) 163 | 164 | //Step9: 165 | var r: Angle = .init(radians: atan(yEquatorialToEcliptic / xEquatorialToEcliptic)) 166 | 167 | //Step9: 168 | switch (yEquatorialToEcliptic >= 0,xEquatorialToEcliptic >= 0){ 169 | 170 | case (true, true): 171 | break 172 | case (true,false): 173 | r.degrees += 180 174 | case(false,true): 175 | r.degrees += 360 176 | case(false,false): 177 | r.degrees += 180 178 | } 179 | 180 | let eclipticLongitude: Angle = .init(degrees: r.degrees) 181 | 182 | return .init(eclipticLatitude: eclipticLatitude, eclipticLongitude: eclipticLongitude) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /Sources/SunKit/Utils.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utils.swift 3 | // 4 | // 5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import Foundation 20 | 21 | let SECONDS_IN_ONE_DAY = 86399 22 | let TWELVE_HOUR_IN_SECONDS: Double = 43200 23 | let TWO_HOURS_IN_SECONDS: Double = 7200 24 | let SECONDS_IN_TEN_MINUTES: Double = 600 25 | let SECONDS_IN_ONE_HOUR: Double = 3600 26 | 27 | 28 | /// - Parameters: 29 | /// - a: first operand 30 | /// - n: second operand 31 | /// - Returns: a % n if a is positive. If a isn't positive, it will add to the result of the modulo operation the value of the n operand until the result is positive.Please note that this function only works when both operands are integers. 32 | public func mod(_ a: Int, _ n: Int) -> Int { 33 | let r = a % n 34 | return r >= 0 ? r : r + n 35 | } 36 | 37 | 38 | /// Same as mod function described above, but this function can accept as first operand a Double and it handle the edge case where a is included between -1 and 0. 39 | /// - Parameters: 40 | /// - a: first operand 41 | /// - n: second operand 42 | /// - Returns: a % n if a is positive. If a isn't positive, it will add to the result of the modulo operation the value of the n operand until the result is positive. 43 | public func extendedMod(_ a: Double, _ n: Int) -> Double { 44 | 45 | let remainder: Double = a.truncatingRemainder(dividingBy: 1) 46 | 47 | if (a < 0 && a > -1){ 48 | 49 | return Double(n) + remainder 50 | } 51 | 52 | let x = Double(mod(Int(a),n)) 53 | 54 | return x + remainder 55 | } 56 | 57 | 58 | 59 | public func clamp(lower: Double, upper: Double, number: Double) -> Double { 60 | 61 | return min(upper,max(lower,number)) 62 | } 63 | 64 | 65 | /// Creates a date with the UTC timezone 66 | /// - Parameters: 67 | /// - day: day of the date you want to create 68 | /// - month: month of the date you want to create 69 | /// - year: year of the date you want to create 70 | /// - hour: hour of the date you want to create 71 | /// - minute: minute of the date you want to create 72 | /// - seconds: second of the date you want to create 73 | /// - nanosecond: nanosecond of the date you want to create 74 | /// - Returns: A date with the parameters given in input. Used in combination with function jdFromDate, that accepts dates in UTC format 75 | public func createDateUTC(day: Int,month: Int,year: Int,hour: Int,minute: Int,seconds: Int, nanosecond: Int = 0) -> Date{ 76 | 77 | var calendarUTC:Calendar = .init(identifier: .gregorian) 78 | calendarUTC.timeZone = .init(secondsFromGMT: 0)! 79 | var dateComponents = DateComponents() 80 | dateComponents.year = year 81 | dateComponents.month = month 82 | dateComponents.day = day 83 | dateComponents.hour = hour 84 | dateComponents.minute = minute 85 | dateComponents.second = seconds 86 | dateComponents.nanosecond = nanosecond 87 | 88 | return calendarUTC.date(from: dateComponents) ?? Date() 89 | } 90 | 91 | 92 | /// Creates a date with the timezone used in your current device 93 | /// - Parameters: 94 | /// - day: day of the date you want to create 95 | /// - month: month of the date you want to create 96 | /// - year: year of the date you want to create 97 | /// - hour: hour of the date you want to create 98 | /// - minute: minute of the date you want to create 99 | /// - seconds: second of the date you want to create 100 | /// - nanosecond: nanosecond of the date you want to create 101 | /// - Returns: A date with the parameters given in input 102 | public func createDateCurrentTimeZone(day: Int,month: Int,year: Int,hour: Int,minute: Int,seconds: Int, nanosecond: Int = 0) -> Date{ 103 | 104 | var calendar: Calendar = .init(identifier: .gregorian) 105 | calendar.timeZone = .current 106 | var dateComponents = DateComponents() 107 | dateComponents.year = year 108 | dateComponents.month = month 109 | dateComponents.day = day 110 | dateComponents.hour = hour 111 | dateComponents.minute = minute 112 | dateComponents.second = seconds 113 | dateComponents.nanosecond = nanosecond 114 | 115 | return calendar.date(from: dateComponents) ?? Date() 116 | } 117 | 118 | /// Creates a date with a custom timezone 119 | /// - Parameters: 120 | /// - day: day of the date you want to create 121 | /// - month: month of the date you want to create 122 | /// - year: year of the date you want to create 123 | /// - hour: hour of the date you want to create 124 | /// - minute: minute of the date you want to create 125 | /// - seconds: second of the date you want to create 126 | /// - nanosecond: nanosecond of the date you want to create 127 | /// - timeZone: timezone of the date 128 | /// - Returns: A date with the parameters given in input 129 | public func createDateCustomTimeZone(day: Int,month: Int,year: Int,hour: Int,minute: Int,seconds: Int, nanosecond: Int = 0, timeZone: TimeZone) -> Date{ 130 | 131 | var calendar: Calendar = .init(identifier: .gregorian) 132 | calendar.timeZone = timeZone 133 | var dateComponents = DateComponents() 134 | dateComponents.year = year 135 | dateComponents.month = month 136 | dateComponents.day = day 137 | dateComponents.hour = hour 138 | dateComponents.minute = minute 139 | dateComponents.second = seconds 140 | dateComponents.nanosecond = nanosecond 141 | 142 | return calendar.date(from: dateComponents) ?? Date() 143 | } 144 | 145 | 146 | /// Converts Julian Number in a date 147 | /// - Parameter jd: Julian number to convert in an UTC date 148 | /// - Returns: The date corresponding to the julian number in input 149 | public func dateFromJd(jd : Double) -> Date { 150 | let JD_JAN_1_1970_0000GMT = 2440587.5 151 | return Date(timeIntervalSince1970: (jd - JD_JAN_1_1970_0000GMT) * 86400) 152 | } 153 | 154 | 155 | /// Converts date in his Julian Number 156 | /// - Parameter date: UTC date to convert in julian number. TimeZone of the given date shall be equals to +0000. 157 | /// - Returns: The julian day number corresponding to date in input 158 | public func jdFromDate(date : Date) -> Double { 159 | let JD_JAN_1_1970_0000GMT = 2440587.5 160 | return JD_JAN_1_1970_0000GMT + date.timeIntervalSince1970 161 | / 86400 162 | } 163 | 164 | 165 | /// Converts UT time to Greenwich Sidereal Time 166 | /// - Parameter ut: UT time to convert in GST 167 | /// - Parameter timeZoneInSeconds: time zone expressed in seconds of your local civil time 168 | /// - Returns: GST equivalent of the UT given in input 169 | public func uT2GST(_ ut:Date) -> HMS{ 170 | 171 | var calendarUTC: Calendar = .init(identifier: .gregorian) 172 | calendarUTC.timeZone = TimeZone(identifier: "GMT")! 173 | 174 | //Step1: 175 | let jd = jdFromDate(date: calendarUTC.startOfDay(for: ut)) 176 | 177 | //Step2: 178 | let year = calendarUTC.component(.year, from: ut) 179 | let firstDayOfTheYear = calendarUTC.date(from: DateComponents(year: year , month: 1, day: 1)) ?? Date() 180 | let jdZero = jdFromDate(date: firstDayOfTheYear) 181 | 182 | //Step3: 183 | let days = jd - jdZero 184 | 185 | //Step4: 186 | let T = (jdZero - 2415020.0) / 36525.0 187 | 188 | //Step5: 189 | let R = 6.6460656 + 2400.051262 * T + 0.00002581*(T*T) 190 | 191 | //Step6: 192 | let B :Double = 24 - R + Double(24 * (year - 1900)) 193 | 194 | //Step7: 195 | let TZero = 0.0657098 * days - B 196 | 197 | //Step8: 198 | let utDecimal = HMS.init(from: ut).hMS2Decimal() 199 | 200 | //Step9: 201 | var gstDecimal = TZero + 1.002738 * utDecimal 202 | 203 | if gstDecimal < 0 { 204 | 205 | gstDecimal += 24 206 | 207 | }else if gstDecimal >= 24 { 208 | 209 | gstDecimal -= 24 210 | } 211 | 212 | let gstHMS = HMS.init(decimal: gstDecimal) 213 | 214 | return gstHMS 215 | } 216 | 217 | /// Converts GST to Local Sidereal Time 218 | /// - Parameters: 219 | /// - gst: GST time to convert in LST 220 | /// - longitude: longitude of the observer 221 | /// - Parameter timeZoneInSeconds: time zone expressed in seconds of your local civil time 222 | /// - Returns: LST equivalent for the GST given in input 223 | public func gST2LST(_ gst: HMS, longitude: Angle) -> HMS{ 224 | 225 | //Step1: 226 | let gstDecimal = gst.hMS2Decimal() 227 | 228 | //Step2: 229 | let adjustment: Double = longitude.degrees / 15.0 230 | 231 | //Step3: 232 | var lstDecimal = gstDecimal + adjustment 233 | 234 | if lstDecimal < 0 { 235 | 236 | lstDecimal += 24 237 | }else if lstDecimal >= 24 { 238 | 239 | lstDecimal -= 24 240 | } 241 | 242 | let lstHMS = HMS.init(decimal: lstDecimal) 243 | 244 | return lstHMS 245 | } 246 | 247 | 248 | /// Converts the number of seconds in HH:MM:ss 249 | /// - Parameter seconds: Number of seconds that have to be converted 250 | /// - Returns: From value in input the equivalent in (hours,minute,seconds) 251 | public func secondsToHoursMinutesSeconds(_ seconds : Int) -> (Int,Int,Int) { 252 | 253 | return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60) 254 | } 255 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Resources/Settings.bundle/SunKit.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | Apache License 10 | Version 2.0, January 2004 11 | http://www.apache.org/licenses/ 12 | 13 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 14 | 15 | 1. Definitions. 16 | 17 | "License" shall mean the terms and conditions for use, reproduction, 18 | and distribution as defined by Sections 1 through 9 of this document. 19 | 20 | "Licensor" shall mean the copyright owner or entity authorized by 21 | the copyright owner that is granting the License. 22 | 23 | "Legal Entity" shall mean the union of the acting entity and all 24 | other entities that control, are controlled by, or are under common 25 | control with that entity. For the purposes of this definition, 26 | "control" means (i) the power, direct or indirect, to cause the 27 | direction or management of such entity, whether by contract or 28 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 29 | outstanding shares, or (iii) beneficial ownership of such entity. 30 | 31 | "You" (or "Your") shall mean an individual or Legal Entity 32 | exercising permissions granted by this License. 33 | 34 | "Source" form shall mean the preferred form for making modifications, 35 | including but not limited to software source code, documentation 36 | source, and configuration files. 37 | 38 | "Object" form shall mean any form resulting from mechanical 39 | transformation or translation of a Source form, including but 40 | not limited to compiled object code, generated documentation, 41 | and conversions to other media types. 42 | 43 | "Work" shall mean the work of authorship, whether in Source or 44 | Object form, made available under the License, as indicated by a 45 | copyright notice that is included in or attached to the work 46 | (an example is provided in the Appendix below). 47 | 48 | "Derivative Works" shall mean any work, whether in Source or Object 49 | form, that is based on (or derived from) the Work and for which the 50 | editorial revisions, annotations, elaborations, or other modifications 51 | represent, as a whole, an original work of authorship. For the purposes 52 | of this License, Derivative Works shall not include works that remain 53 | separable from, or merely link (or bind by name) to the interfaces of, 54 | the Work and Derivative Works thereof. 55 | 56 | "Contribution" shall mean any work of authorship, including 57 | the original version of the Work and any modifications or additions 58 | to that Work or Derivative Works thereof, that is intentionally 59 | submitted to Licensor for inclusion in the Work by the copyright owner 60 | or by an individual or Legal Entity authorized to submit on behalf of 61 | the copyright owner. For the purposes of this definition, "submitted" 62 | means any form of electronic, verbal, or written communication sent 63 | to the Licensor or its representatives, including but not limited to 64 | communication on electronic mailing lists, source code control systems, 65 | and issue tracking systems that are managed by, or on behalf of, the 66 | Licensor for the purpose of discussing and improving the Work, but 67 | excluding communication that is conspicuously marked or otherwise 68 | designated in writing by the copyright owner as "Not a Contribution." 69 | 70 | "Contributor" shall mean Licensor and any individual or Legal Entity 71 | on behalf of whom a Contribution has been received by Licensor and 72 | subsequently incorporated within the Work. 73 | 74 | 2. Grant of Copyright License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | copyright license to reproduce, prepare Derivative Works of, 78 | publicly display, publicly perform, sublicense, and distribute the 79 | Work and such Derivative Works in Source or Object form. 80 | 81 | 3. Grant of Patent License. Subject to the terms and conditions of 82 | this License, each Contributor hereby grants to You a perpetual, 83 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 84 | (except as stated in this section) patent license to make, have made, 85 | use, offer to sell, sell, import, and otherwise transfer the Work, 86 | where such license applies only to those patent claims licensable 87 | by such Contributor that are necessarily infringed by their 88 | Contribution(s) alone or by combination of their Contribution(s) 89 | with the Work to which such Contribution(s) was submitted. If You 90 | institute patent litigation against any entity (including a 91 | cross-claim or counterclaim in a lawsuit) alleging that the Work 92 | or a Contribution incorporated within the Work constitutes direct 93 | or contributory patent infringement, then any patent licenses 94 | granted to You under this License for that Work shall terminate 95 | as of the date such litigation is filed. 96 | 97 | 4. Redistribution. You may reproduce and distribute copies of the 98 | Work or Derivative Works thereof in any medium, with or without 99 | modifications, and in Source or Object form, provided that You 100 | meet the following conditions: 101 | 102 | (a) You must give any other recipients of the Work or 103 | Derivative Works a copy of this License; and 104 | 105 | (b) You must cause any modified files to carry prominent notices 106 | stating that You changed the files; and 107 | 108 | (c) You must retain, in the Source form of any Derivative Works 109 | that You distribute, all copyright, patent, trademark, and 110 | attribution notices from the Source form of the Work, 111 | excluding those notices that do not pertain to any part of 112 | the Derivative Works; and 113 | 114 | (d) If the Work includes a "NOTICE" text file as part of its 115 | distribution, then any Derivative Works that You distribute must 116 | include a readable copy of the attribution notices contained 117 | within such NOTICE file, excluding those notices that do not 118 | pertain to any part of the Derivative Works, in at least one 119 | of the following places: within a NOTICE text file distributed 120 | as part of the Derivative Works; within the Source form or 121 | documentation, if provided along with the Derivative Works; or, 122 | within a display generated by the Derivative Works, if and 123 | wherever such third-party notices normally appear. The contents 124 | of the NOTICE file are for informational purposes only and 125 | do not modify the License. You may add Your own attribution 126 | notices within Derivative Works that You distribute, alongside 127 | or as an addendum to the NOTICE text from the Work, provided 128 | that such additional attribution notices cannot be construed 129 | as modifying the License. 130 | 131 | You may add Your own copyright statement to Your modifications and 132 | may provide additional or different license terms and conditions 133 | for use, reproduction, or distribution of Your modifications, or 134 | for any such Derivative Works as a whole, provided Your use, 135 | reproduction, and distribution of the Work otherwise complies with 136 | the conditions stated in this License. 137 | 138 | 5. Submission of Contributions. Unless You explicitly state otherwise, 139 | any Contribution intentionally submitted for inclusion in the Work 140 | by You to the Licensor shall be under the terms and conditions of 141 | this License, without any additional terms or conditions. 142 | Notwithstanding the above, nothing herein shall supersede or modify 143 | the terms of any separate license agreement you may have executed 144 | with Licensor regarding such Contributions. 145 | 146 | 6. Trademarks. This License does not grant permission to use the trade 147 | names, trademarks, service marks, or product names of the Licensor, 148 | except as required for reasonable and customary use in describing the 149 | origin of the Work and reproducing the content of the NOTICE file. 150 | 151 | 7. Disclaimer of Warranty. Unless required by applicable law or 152 | agreed to in writing, Licensor provides the Work (and each 153 | Contributor provides its Contributions) on an "AS IS" BASIS, 154 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 155 | implied, including, without limitation, any warranties or conditions 156 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 157 | PARTICULAR PURPOSE. You are solely responsible for determining the 158 | appropriateness of using or redistributing the Work and assume any 159 | risks associated with Your exercise of permissions under this License. 160 | 161 | 8. Limitation of Liability. In no event and under no legal theory, 162 | whether in tort (including negligence), contract, or otherwise, 163 | unless required by applicable law (such as deliberate and grossly 164 | negligent acts) or agreed to in writing, shall any Contributor be 165 | liable to You for damages, including any direct, indirect, special, 166 | incidental, or consequential damages of any character arising as a 167 | result of this License or out of the use or inability to use the 168 | Work (including but not limited to damages for loss of goodwill, 169 | work stoppage, computer failure or malfunction, or any and all 170 | other commercial damages or losses), even if such Contributor 171 | has been advised of the possibility of such damages. 172 | 173 | 9. Accepting Warranty or Additional Liability. While redistributing 174 | the Work or Derivative Works thereof, You may choose to offer, 175 | and charge a fee for, acceptance of support, warranty, indemnity, 176 | or other liability obligations and/or rights consistent with this 177 | License. However, in accepting such obligations, You may act only 178 | on Your own behalf and on Your sole responsibility, not on behalf 179 | of any other Contributor, and only if You agree to indemnify, 180 | defend, and hold each Contributor harmless for any liability 181 | incurred by, or claims asserted against, such Contributor by reason 182 | of your accepting any such warranty or additional liability. 183 | 184 | END OF TERMS AND CONDITIONS 185 | 186 | APPENDIX: How to apply the Apache License to your work. 187 | 188 | To apply the Apache License to your work, attach the following 189 | boilerplate notice, with the fields enclosed by brackets "[]" 190 | replaced with your own identifying information. (Don't include 191 | the brackets!) The text should be enclosed in the appropriate 192 | comment syntax for the file format. We also recommend that a 193 | file or class name and description of purpose be included on the 194 | same "printed page" as the copyright notice for easier 195 | identification within third-party archives. 196 | 197 | Copyright 2023 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 198 | 199 | Licensed under the Apache License, Version 2.0 (the "License"); 200 | you may not use this file except in compliance with the License. 201 | You may obtain a copy of the License at 202 | 203 | http://www.apache.org/licenses/LICENSE-2.0 204 | 205 | Unless required by applicable law or agreed to in writing, software 206 | distributed under the License is distributed on an "AS IS" BASIS, 207 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 208 | See the License for the specific language governing permissions and 209 | limitations under the License. 210 | 211 | Type 212 | PSGroupSpecifier 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /Tests/SunKitTests/UT_Sun.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UT_Sun.swift 3 | // 4 | // 5 | // Copyright 2023 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import XCTest 20 | import Foundation 21 | import CoreLocation 22 | @testable import SunKit 23 | 24 | final class UT_Sun: XCTestCase { 25 | 26 | /*-------------------------------------------------------------------- 27 | Thresholds. UTs will pass if |output - expectedOutput| < threshold 28 | *-------------------------------------------------------------------*/ 29 | static let sunAzimuthThreshold: Double = 0.05 30 | static let sunAltitudeThreshold: Double = 0.1 31 | static let sunSetRiseThresholdInSeconds: Double = 120 //2 minutes in seconds 32 | static let sunEquinoxesAndSolsticesThresholdInSeconds: Double = 700 // approxametly 11 minutes 33 | 34 | static let objectShadowThreshold: Double = 0.01 35 | 36 | /*-------------------------------------------------------------------- 37 | Naples timezone and location 38 | *-------------------------------------------------------------------*/ 39 | static let naplesLocation: CLLocation = .init(latitude: 40.84014, longitude: 14.25226) 40 | static let timeZoneNaples = 1 41 | static let timeZoneNaplesDaylightSaving = 2 42 | 43 | /*-------------------------------------------------------------------- 44 | Tokyo timezone and location 45 | *-------------------------------------------------------------------*/ 46 | static let tokyoLocation: CLLocation = .init(latitude: 35.68946, longitude: 139.69172) 47 | static let timeZoneTokyo = 9 48 | 49 | /*-------------------------------------------------------------------- 50 | Louisa USA timezone and location 51 | *-------------------------------------------------------------------*/ 52 | static let louisaLocation: CLLocation = .init(latitude: 38, longitude: -78) 53 | static let timeZoneLouisa = -5 54 | static let timeZoneLouisaDaylightSaving = -4 55 | 56 | /*-------------------------------------------------------------------- 57 | Tromso circumpolar timezone and location 58 | *-------------------------------------------------------------------*/ 59 | static let tromsoLocation: CLLocation = .init(latitude: 69.6489, longitude: 18.95508) 60 | static let timeZoneTromso = 1 61 | static let timeZoneTromsoDaylightSaving = 2 62 | 63 | /*-------------------------------------------------------------------- 64 | Mumbai timezone and location 65 | *-------------------------------------------------------------------*/ 66 | static let mumbaiLocation: CLLocation = .init(latitude: 18.94017, longitude: 72.83489) 67 | static let timeZoneMumbai = 5.5 68 | 69 | 70 | /// Test of Sun azimuth, sunrise, sunset, evening golden hour start and evening golden hour end 71 | /// Value for expected results have been taken from SunCalc.org 72 | func testOfSun() throws { 73 | 74 | /*-------------------------------------------------------------------- 75 | Naples 76 | *-------------------------------------------------------------------*/ 77 | 78 | //Test1: 19/11/22 20:00. Timezone +1. 79 | 80 | //Step1: Creating Sun instance in Naples and with timezone +1 81 | var timeZoneUnderTest: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current 82 | var sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneUnderTest) 83 | 84 | //Step2: Setting 19/11/22 20:00 as date. (No daylight saving) 85 | var dateUnderTest = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 20, minute: 00, seconds: 00,timeZone: timeZoneUnderTest) 86 | sunUnderTest.setDate(dateUnderTest) 87 | 88 | //Step3: Saving expected outputs 89 | var expectedAzimuth = 275.84 90 | var expectedAltitude = -37.34 91 | 92 | var expectedSunRise = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 6, minute: 54, seconds: 12,timeZone: timeZoneUnderTest) 93 | var expectedSunset = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 16, minute: 42, seconds: 07,timeZone: timeZoneUnderTest) 94 | 95 | var expectedGoldenHourStart = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 16, minute: 00, seconds: 00,timeZone: timeZoneUnderTest) 96 | var expectedGoldenHourEnd = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 16, minute: 59, seconds: 00,timeZone: timeZoneUnderTest) 97 | 98 | var expectedcivilDawn = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 6, minute: 24, seconds: 51,timeZone: timeZoneUnderTest) 99 | var expectedcivilDusk = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 17, minute: 11, seconds: 28,timeZone: timeZoneUnderTest) 100 | 101 | var expectedSolarNoon = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 11, minute: 48, seconds: 21,timeZone: timeZoneUnderTest) 102 | 103 | let expectednauticalDawn = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 5, minute: 52, seconds: 21,timeZone: timeZoneUnderTest) 104 | 105 | let expectednauticalDusk = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 17, minute: 44, seconds: 45,timeZone: timeZoneUnderTest) 106 | 107 | let expectedastronomicalDawn = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 5, minute: 19, seconds: 25,timeZone: timeZoneUnderTest) 108 | 109 | let expectedastronomicalDusk = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 18, minute: 17, seconds: 20,timeZone: timeZoneUnderTest) 110 | 111 | 112 | //Step4: Check if the output are close to the expected ones 113 | 114 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) 115 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) 116 | 117 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 118 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 119 | 120 | XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 121 | XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 122 | 123 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 124 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 125 | 126 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 127 | 128 | XCTAssertTrue(abs(expectednauticalDusk.timeIntervalSince1970 - sunUnderTest.nauticalDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 129 | XCTAssertTrue(abs(expectednauticalDawn.timeIntervalSince1970 - sunUnderTest.nauticalDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 130 | 131 | XCTAssertTrue(abs(expectedastronomicalDusk.timeIntervalSince1970 - sunUnderTest.astronomicalDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 132 | 133 | XCTAssertTrue(abs(expectedastronomicalDawn.timeIntervalSince1970 - sunUnderTest.astronomicalDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 134 | 135 | 136 | //Test: 31/12/2024 15:32. Timezone +1. Leap Year. 137 | 138 | //Step1: Creating Sun instance in Naples and with timezone +1 139 | timeZoneUnderTest = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current 140 | 141 | sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneUnderTest) 142 | 143 | //Step2: Setting 31/12/2024 15:32 as date. (No daylight saving) 144 | dateUnderTest = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 15, minute: 32, seconds: 00,timeZone: timeZoneUnderTest) 145 | sunUnderTest.setDate(dateUnderTest) 146 | 147 | //Step3: Saving expected outputs 148 | expectedAzimuth = 226.99 149 | expectedAltitude = 10.35 150 | 151 | expectedSunRise = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 7, minute: 26, seconds: 57,timeZone: timeZoneUnderTest) 152 | expectedSunset = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 16, minute: 45, seconds: 32,timeZone: timeZoneUnderTest) 153 | 154 | expectedGoldenHourStart = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 16, minute: 02, seconds: 00,timeZone: timeZoneUnderTest) 155 | expectedGoldenHourEnd = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 17, minute: 05, seconds: 00,timeZone: timeZoneUnderTest) 156 | 157 | expectedcivilDawn = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 6, minute: 56, seconds: 24,timeZone: timeZoneUnderTest) 158 | expectedcivilDusk = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 17, minute: 16, seconds: 06,timeZone: timeZoneUnderTest) 159 | 160 | expectedSolarNoon = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 12, minute: 06, seconds: 11,timeZone: timeZoneUnderTest) 161 | 162 | //Step4: Check if the output are close to the expected ones 163 | 164 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) 165 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) 166 | 167 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 168 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 169 | 170 | XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 171 | XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 172 | 173 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 174 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 175 | 176 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 177 | 178 | 179 | /*-------------------------------------------------------------------- 180 | Tokyo 181 | *-------------------------------------------------------------------*/ 182 | 183 | //Test: 1/08/22 16:50. Timezone +9. 184 | 185 | //Step1: Creating sun instance in Tokyo and with timezone +9 186 | timeZoneUnderTest = .init(secondsFromGMT: UT_Sun.timeZoneTokyo * Int(SECONDS_IN_ONE_HOUR)) ?? .current 187 | sunUnderTest = Sun.init(location: UT_Sun.tokyoLocation, timeZone: timeZoneUnderTest) 188 | 189 | //Step2: Setting 1/08/22 16:50 as date. 190 | dateUnderTest = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 16, minute: 50, seconds: 00,timeZone: timeZoneUnderTest) 191 | sunUnderTest.setDate(dateUnderTest) 192 | 193 | //Step3: Saving expected outputs 194 | expectedAzimuth = 276.98 195 | expectedAltitude = 21.90 196 | 197 | expectedSunRise = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 4, minute: 48, seconds: 29,timeZone: timeZoneUnderTest) 198 | expectedSunset = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 18, minute: 46, seconds: 15,timeZone: timeZoneUnderTest) 199 | 200 | expectedGoldenHourStart = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 18, minute: 11, seconds: 00,timeZone: timeZoneUnderTest) 201 | expectedGoldenHourEnd = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 19, minute: 04, seconds: 00,timeZone: timeZoneUnderTest) 202 | 203 | expectedcivilDawn = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 4, minute: 20, seconds: 39,timeZone: timeZoneUnderTest) 204 | expectedcivilDusk = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 19, minute: 14, seconds: 00,timeZone: timeZoneUnderTest) 205 | 206 | expectedSolarNoon = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 11, minute: 47, seconds: 36,timeZone: timeZoneUnderTest) 207 | 208 | //Step4: Check if the output are close to the expected ones 209 | 210 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) 211 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) 212 | 213 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 214 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 215 | 216 | XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 217 | XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 218 | 219 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 220 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 221 | 222 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 223 | 224 | /*-------------------------------------------------------------------- 225 | Louisa USA 226 | *-------------------------------------------------------------------*/ 227 | 228 | //Test: 1/01/15 22:00. Timezone -5. 229 | 230 | //Step1: Creating sun instance in Louisa and with timezone -5 (No daylight saving) 231 | timeZoneUnderTest = .init(secondsFromGMT: UT_Sun.timeZoneLouisa * Int(SECONDS_IN_ONE_HOUR)) ?? .current 232 | sunUnderTest = Sun.init(location: UT_Sun.louisaLocation, timeZone: timeZoneUnderTest) 233 | 234 | //Step2: Setting 1/01/15 22:00 as date. (No daylight saving) 235 | dateUnderTest = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 22, minute: 00, seconds: 00,timeZone: timeZoneUnderTest) 236 | sunUnderTest.setDate(dateUnderTest) 237 | 238 | //Step3: Saving expected outputs 239 | expectedAzimuth = 287.62 240 | expectedAltitude = -57.41 241 | 242 | expectedSunRise = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 7, minute: 27, seconds: 29,timeZone: timeZoneUnderTest) 243 | expectedSunset = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 17, minute: 03, seconds: 25,timeZone: timeZoneUnderTest) 244 | 245 | expectedGoldenHourStart = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 16, minute: 22, seconds: 00,timeZone: timeZoneUnderTest) 246 | expectedGoldenHourEnd = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 17, minute: 22, seconds: 00,timeZone: timeZoneUnderTest) 247 | 248 | expectedcivilDawn = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 6, minute: 58, seconds: 28,timeZone: timeZoneUnderTest) 249 | expectedcivilDusk = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 17, minute: 32, seconds: 27,timeZone: timeZoneUnderTest) 250 | 251 | expectedSolarNoon = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 12, minute: 15, seconds: 23,timeZone: timeZoneUnderTest) 252 | 253 | //Step4: Check if the output are close to the expected ones 254 | 255 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) 256 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) 257 | 258 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 259 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 260 | 261 | XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 262 | XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 263 | 264 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 265 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 266 | 267 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 268 | 269 | /*-------------------------------------------------------------------- 270 | Tromso circumpolar 271 | *-------------------------------------------------------------------*/ 272 | 273 | //Test: 19/01/22 17:31. Timezone +1. 274 | 275 | //Step1: Creating sun instance in Tromso and with timezone +1 (No daylight saving) 276 | timeZoneUnderTest = .init(secondsFromGMT: UT_Sun.timeZoneTromso * Int(SECONDS_IN_ONE_HOUR)) ?? .current 277 | sunUnderTest = Sun.init(location: UT_Sun.tromsoLocation, timeZone: timeZoneUnderTest) 278 | 279 | //Step2: Setting 19/01/22 17:31 as date. (No daylight saving) 280 | dateUnderTest = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 17, minute: 31, seconds: 00,timeZone: timeZoneUnderTest) 281 | sunUnderTest.setDate(dateUnderTest) 282 | 283 | //Step3: Saving expected outputs 284 | expectedAzimuth = 257.20 285 | expectedAltitude = -16.93 286 | 287 | expectedSunRise = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 10, minute: 41, seconds: 46,timeZone: timeZoneUnderTest) 288 | expectedSunset = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 13, minute: 08, seconds: 48,timeZone: timeZoneUnderTest) 289 | expectedcivilDawn = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 08, minute: 45, seconds: 30,timeZone: timeZoneUnderTest) 290 | expectedcivilDusk = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 15, minute: 05, seconds: 08,timeZone: timeZoneUnderTest) 291 | 292 | expectedSolarNoon = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 11, minute: 54, seconds: 52,timeZone: timeZoneUnderTest) 293 | 294 | //Step4: Check if the output are close to the expected ones 295 | 296 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) 297 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) 298 | 299 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 300 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 301 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 302 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 303 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 304 | 305 | 306 | /*-------------------------------------------------------------------- 307 | Mumbai 308 | *-------------------------------------------------------------------*/ 309 | 310 | //Test: 12/03/23 14:46. Timezone +5.5. 311 | 312 | //Step1: Creating Sun instance in Mumbai and with timezone +5.5 313 | timeZoneUnderTest = .init(secondsFromGMT: Int(UT_Sun.timeZoneMumbai * SECONDS_IN_ONE_HOUR)) ?? .current 314 | sunUnderTest = Sun.init(location: UT_Sun.mumbaiLocation, timeZone: timeZoneUnderTest) 315 | 316 | //Step2: Setting 12/03/23 14:46 as date. 317 | dateUnderTest = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 14, minute: 46, seconds: 00,timeZone: timeZoneUnderTest) 318 | sunUnderTest.setDate(dateUnderTest) 319 | 320 | //Step3: Saving expected outputs 321 | expectedAzimuth = 235.41 322 | expectedAltitude = 53.51 323 | 324 | expectedSunRise = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 6, minute: 49, seconds: 35,timeZone: timeZoneUnderTest) 325 | expectedSunset = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 18, minute: 47, seconds: 42,timeZone: timeZoneUnderTest) 326 | expectedcivilDawn = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 6, minute: 27, seconds: 59,timeZone: timeZoneUnderTest) 327 | expectedcivilDusk = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 19, minute: 09, seconds: 19,timeZone: timeZoneUnderTest) 328 | 329 | expectedSolarNoon = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 12, minute: 48, seconds: 31,timeZone: timeZoneUnderTest) 330 | 331 | //Step4: Check if the output are close to the expected ones 332 | 333 | XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) 334 | XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) 335 | 336 | XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 337 | XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 338 | XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 339 | XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 340 | XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) 341 | 342 | 343 | //Test for issue #26 in github 344 | guard let pst = TimeZone(abbreviation: "PST") else { 345 | abort() 346 | } 347 | 348 | guard let utc = TimeZone(abbreviation: "UTC") else { 349 | abort() 350 | } 351 | 352 | let location: CLLocation = .init(latitude: 34.052235, longitude: -118.243683) 353 | 354 | var sun = Sun.init(location: location, timeZone: pst) 355 | 356 | sun.setDate(SunKit.createDateCustomTimeZone(day: 11, month: 3, year: 2023, hour: 22, minute: 00, seconds: 00, timeZone: pst)) 357 | XCTAssertEqual(sun.sunrise.toString(pst), "03/11, 06:08") 358 | XCTAssertEqual(sun.sunset.toString(pst), "03/11, 17:58") 359 | XCTAssertEqual(sun.sunrise.toString(utc), "03/11, 14:08") 360 | XCTAssertEqual(sun.sunset.toString(utc), "03/12, 01:58") 361 | 362 | sun.setDate(SunKit.createDateCustomTimeZone(day: 13, month: 3, year: 2023, hour: 22, minute: 00, seconds: 00, timeZone: pst)) 363 | XCTAssertEqual(sun.sunrise.toString(pst), "03/13, 07:06") 364 | XCTAssertEqual(sun.sunset.toString(pst), "03/13, 18:59") 365 | XCTAssertEqual(sun.sunrise.toString(utc), "03/13, 14:06") 366 | XCTAssertEqual(sun.sunset.toString(utc), "03/14, 01:59") 367 | 368 | } 369 | 370 | func testOfObjectShadow() throws{ 371 | 372 | //Step1: Creating sun instance in Naples and with timezone +1 (No daylight saving) 373 | let timeZoneUnderTest: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current 374 | // let timeZoneDaylightSaving: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaplesDaylightSaving * Int(SECONDS_IN_ONE_HOUR)) ?? .current 375 | let sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneUnderTest) 376 | 377 | 378 | 379 | XCTAssertTrue(abs(sunUnderTest.shadowLength(with: .init(degrees: 43.40))! - 1.06 ) <= UT_Sun.objectShadowThreshold) 380 | 381 | XCTAssertTrue(sunUnderTest.shadowLength(with: .init(degrees: -0.01)) == nil) 382 | 383 | XCTAssertTrue(sunUnderTest.shadowLength(with: .init(degrees: 90)) == 0) 384 | 385 | 386 | } 387 | 388 | func testOfEquinoxesAndSolstices() throws { 389 | 390 | //Test: 19/01/22 17:31. Timezone +1. Naples 391 | 392 | //Step1: Creating sun instance in Naples and with timezone +1 (No daylight saving) 393 | let timeZoneUnderTest: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current 394 | let timeZoneDaylightSaving: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaplesDaylightSaving * Int(SECONDS_IN_ONE_HOUR)) ?? .current 395 | var sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneUnderTest) 396 | 397 | //Step2: Setting 19/01/22 17:31 as date. (No daylight saving) 398 | let dateUnderTest = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 17, minute: 31, seconds: 00,timeZone: timeZoneUnderTest) 399 | sunUnderTest.setDate(dateUnderTest) 400 | 401 | //Step3: Saving expected outputs 402 | 403 | let expectedMarchEquinox = createDateCustomTimeZone(day: 20, month: 3, year: 2022, hour: 16, minute: 33, seconds: 00,timeZone: timeZoneUnderTest) 404 | let expectedJuneSolstice = createDateCustomTimeZone(day: 21, month: 6, year: 2022, hour: 11, minute: 13, seconds: 00,timeZone: timeZoneDaylightSaving) 405 | let expectedSeptemberEquinox = createDateCustomTimeZone(day: 23, month: 09, year: 2022, hour: 03, minute: 03, seconds: 00,timeZone: timeZoneDaylightSaving) 406 | let expectedDecemberSolstice = createDateCustomTimeZone(day: 21, month: 12, year: 2022, hour: 22, minute: 47, seconds: 00,timeZone: timeZoneUnderTest) 407 | 408 | //Step4: Check if the output are close to the expected ones 409 | XCTAssertTrue(abs(expectedMarchEquinox.timeIntervalSince1970 - sunUnderTest.marchEquinox.timeIntervalSince1970) 410 | < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds) 411 | 412 | XCTAssertTrue(abs(expectedJuneSolstice.timeIntervalSince1970 - sunUnderTest.juneSolstice.timeIntervalSince1970) 413 | < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds) 414 | 415 | XCTAssertTrue(abs(expectedSeptemberEquinox.timeIntervalSince1970 - sunUnderTest.septemberEquinox.timeIntervalSince1970) 416 | < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds) 417 | 418 | XCTAssertTrue(abs(expectedDecemberSolstice.timeIntervalSince1970 - sunUnderTest.decemberSolstice.timeIntervalSince1970) 419 | < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds) 420 | 421 | } 422 | 423 | 424 | /// Test of a sun ionstance whenm you play with timezones and change location 425 | func testOfSunWhenTimezoneChanges() throws { 426 | 427 | //Step1: Creating Sun instance in Naples and with timezone +1 428 | let timeZoneNaples: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current 429 | var sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneNaples) 430 | 431 | //Step2: Setting 19/11/22 20:00 as date. (No daylight saving) 432 | let dateUnderTest = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 20, minute: 00, seconds: 00,timeZone: timeZoneNaples) 433 | sunUnderTest.setDate(dateUnderTest) 434 | 435 | //Step3: Change location and timezone 436 | let timeZoneTokyo: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneTokyo * Int(SECONDS_IN_ONE_HOUR)) ?? .current 437 | sunUnderTest.setLocation(UT_Sun.tokyoLocation, timeZoneTokyo) 438 | 439 | //Step4: Saving expected outputs for all the date 440 | let expectedDate = createDateCustomTimeZone(day: 20, month: 11, year: 2022, hour: 4, minute: 00, seconds: 00,timeZone: timeZoneTokyo) 441 | 442 | //Step5: Check if output of sunUnderTest.date matches the expected output 443 | XCTAssertTrue(expectedDate == sunUnderTest.date) 444 | 445 | } 446 | 447 | 448 | 449 | func testPerformance() throws { 450 | // Performance of setDate function that will refresh all the sun variables 451 | 452 | //Step1: Creating sun instance in Naples with timezone +1 453 | var sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: Double(UT_Sun.timeZoneNaples)) 454 | 455 | //Step2: Setting 19/11/22 20:00 as date. 456 | let dateUnderTest = createDateCurrentTimeZone(day: 19, month: 11, year: 2022, hour: 20, minute: 00, seconds: 00) 457 | 458 | self.measure { 459 | sunUnderTest.setDate(dateUnderTest) 460 | } 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /Sources/SunKit/Sun.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sun.swift 3 | // 4 | // 5 | // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano 6 | // 7 | // Licensed under the Apache License, Version 2.0 (the "License"); 8 | // you may not use this file except in compliance with the License. 9 | // You may obtain a copy of the License at 10 | // 11 | // http://www.apache.org/licenses/LICENSE-2.0 12 | // 13 | // Unless required by applicable law or agreed to in writing, software 14 | // distributed under the License is distributed on an "AS IS" BASIS, 15 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | // See the License for the specific language governing permissions and 17 | // limitations under the License. 18 | 19 | import Foundation 20 | import CoreLocation 21 | 22 | public struct Sun: Identifiable, Sendable { 23 | public let id: UUID = UUID() 24 | 25 | /*-------------------------------------------------------------------- 26 | Public get Variables 27 | *-------------------------------------------------------------------*/ 28 | 29 | public private(set) var location: CLLocation 30 | public private(set) var timeZone: TimeZone 31 | public private(set) var date: Date 32 | 33 | /*-------------------------------------------------------------------- 34 | Sun Events during the day 35 | *-------------------------------------------------------------------*/ 36 | 37 | ///Date of Sunrise 38 | public private(set) var sunrise: Date = Date() 39 | ///Date of Sunset 40 | public private(set) var sunset: Date = Date() 41 | ///Date of Solar Noon for 42 | public private(set) var solarNoon: Date = Date() 43 | ///Date of Solar Midnight 44 | public private(set) var solarMidnight: Date = Date() 45 | 46 | ///Date at which evening evening Golden hour starts 47 | public private(set) var eveningGoldenHourStart: Date = Date() 48 | ///Date at which evening evening Golden hour ends 49 | public private(set) var eveningGoldenHourEnd: Date = Date() 50 | 51 | ///Date at which evening Morning Golden hour starts 52 | public private(set) var morningGoldenHourStart: Date = Date() 53 | ///Date at which evening Morning Golden hour ends 54 | public private(set) var morningGoldenHourEnd: Date = Date() 55 | 56 | 57 | ///Date at which there is the Civil Dusk 58 | public private(set) var civilDusk: Date = Date() 59 | ///Date at which there is the Civil Dawn 60 | public private(set) var civilDawn: Date = Date() 61 | 62 | ///Date at which there is the Nautical Dusk 63 | public private(set) var nauticalDusk: Date = Date() 64 | ///Date at which there is the Nautical Dawn 65 | public private(set) var nauticalDawn: Date = Date() 66 | 67 | ///Date at which there is the Astronomical Dusk 68 | public private(set) var astronomicalDusk: Date = Date() 69 | ///Date at which there is the Astronomical Dawn 70 | public private(set) var astronomicalDawn: Date = Date() 71 | 72 | ///Date at which morning Blue Hour starts. Sun at -6 degrees elevation = civil dusk 73 | public var morningBlueHourStart: Date{ 74 | civilDawn 75 | } 76 | 77 | ///Date at which morning Blue Hour ends. Sun at -4 degrees elevation = morning golden hour start 78 | public var morningBlueHourEnd: Date { 79 | morningGoldenHourStart 80 | } 81 | 82 | ///Date at which evening Blue Hour starts. Sun at -4 degrees elevation = evening golden hour end 83 | public var eveningBlueHourStart: Date{ 84 | eveningGoldenHourEnd 85 | } 86 | 87 | ///Date at which morning Blue Hour ends. Sun at -6 degrees elevation = Civil Dawn 88 | public var eveningBlueHourEnd: Date { 89 | civilDusk 90 | } 91 | 92 | 93 | /*-------------------------------------------------------------------- 94 | Sun Azimuths for Self.date and for Sunrise,Sunset and Solar Noon 95 | *-------------------------------------------------------------------*/ 96 | 97 | ///Azimuth of Sunrise 98 | public private(set) var sunriseAzimuth: Double = 0 99 | ///Azimuth of Sunset 100 | public private(set) var sunsetAzimuth: Double = 0 101 | ///Azimuth of Solar noon 102 | public private(set) var solarNoonAzimuth: Double = 0 103 | 104 | // Sun azimuth for (Location,Date) in Self 105 | public var azimuth: Angle { 106 | sunHorizonCoordinates.azimuth 107 | } 108 | 109 | // Sun altitude for (Location,Date) in Self 110 | public var altitude: Angle { 111 | sunHorizonCoordinates.altitude 112 | } 113 | 114 | public private(set) var sunEquatorialCoordinates: EquatorialCoordinates = .init(declination: .zero) 115 | public private(set) var sunEclipticCoordinates: EclipticCoordinates = .init(eclipticLatitude: .zero, eclipticLongitude: .zero) 116 | 117 | /*-------------------------------------------------------------------- 118 | Sun Events during the year 119 | *-------------------------------------------------------------------*/ 120 | 121 | ///Date at which there will be march equinox 122 | public private(set) var marchEquinox: Date = Date() 123 | ///Date at which there will be june solstice 124 | public private(set) var juneSolstice: Date = Date() 125 | ///Date at which there will be september solstice 126 | public private(set) var septemberEquinox: Date = Date() 127 | ///Date at which there will be december solstice 128 | public private(set) var decemberSolstice: Date = Date() 129 | 130 | /*-------------------------------------------------------------------- 131 | Nice To Have public variables 132 | *-------------------------------------------------------------------*/ 133 | 134 | /// Longitude of location 135 | public var longitude: Angle { 136 | .init(degrees: location.coordinate.longitude) 137 | } 138 | 139 | /// Latitude of Location 140 | public var latitude: Angle { 141 | .init(degrees: location.coordinate.latitude) 142 | } 143 | 144 | /// Returns daylight time in seconds 145 | public var totalDayLightTime: Int { 146 | let diffComponents = calendar.dateComponents([.second], from: sunrise, to: sunset) 147 | 148 | return diffComponents.second ?? 0 149 | } 150 | 151 | /// Returns night time in seconds 152 | public var totalNightTime: Int { 153 | let startOfTheDay = calendar.startOfDay(for: date) 154 | let endOfTheDay = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: startOfTheDay)! 155 | var diffComponents = calendar.dateComponents([.second], from: startOfTheDay, to: sunrise) 156 | var nightHours: Int = diffComponents.second ?? 0 157 | diffComponents = calendar.dateComponents([.second], from: sunset, to: endOfTheDay) 158 | nightHours = nightHours + (diffComponents.second ?? 0) 159 | 160 | return nightHours 161 | } 162 | 163 | /// Returns True if is night 164 | public var isNight: Bool { 165 | if !isCircumPolar { 166 | date < sunrise || date > sunset 167 | } else { 168 | isAlwaysNight 169 | } 170 | } 171 | 172 | /// Returns True if is twilight time 173 | public var isTwilight: Bool { 174 | (astronomicalDawn <= date && date < sunrise) || (sunset < date && date <= astronomicalDusk) 175 | } 176 | 177 | /// Returns True if we are in evening golden hour range 178 | public var isEveningGoldenHour: Bool { 179 | date >= eveningGoldenHourStart && date <= eveningGoldenHourEnd 180 | } 181 | 182 | /// Returns True if we are in morning golden hour range 183 | public var isMorningGoldenHour: Bool { 184 | date >= morningGoldenHourStart && date <= morningGoldenHourEnd 185 | } 186 | 187 | /// Returns True if we are in golden hour range 188 | public var isGoldenHour: Bool { 189 | isMorningGoldenHour || isEveningGoldenHour 190 | } 191 | 192 | /// Returns True if we are in evening blue hour range 193 | public var isEveningBlueHour: Bool { 194 | date >= eveningBlueHourStart && date <= eveningBlueHourEnd 195 | } 196 | 197 | /// Returns True if we are in morning blue hour range 198 | public var isMorningBlueHour: Bool { 199 | date >= morningBlueHourStart && date <= morningBlueHourEnd 200 | } 201 | 202 | /// Returns True if we are in blue hour range 203 | public var isBlueHour: Bool { 204 | isMorningBlueHour || isEveningBlueHour 205 | } 206 | 207 | 208 | /// Returns true if we are near the pole and we are in a situation in which Sun Events during the day could have no meaning 209 | public var isCircumPolar: Bool { 210 | isAlwaysDay || isAlwaysNight 211 | } 212 | 213 | /// Returns true if for (Location,Date) is always daylight (e.g Tromso city in summer) 214 | public var isAlwaysDay: Bool { 215 | let startOfTheDay = calendar.startOfDay(for: date) 216 | let almostNextDay = startOfTheDay + Double(SECONDS_IN_ONE_DAY) 217 | 218 | return sunset == almostNextDay || sunrise < startOfTheDay + SECONDS_IN_TEN_MINUTES 219 | } 220 | 221 | /// Returns true if for (Location,Date) is always daylight (e.g Tromso city in Winter) 222 | public var isAlwaysNight: Bool { 223 | sunset - TWO_HOURS_IN_SECONDS < sunrise 224 | } 225 | 226 | /*-------------------------------------------------------------------- 227 | Initializers 228 | *-------------------------------------------------------------------*/ 229 | 230 | public init(location: CLLocation, timeZone: TimeZone, date: Date = Date()) { 231 | self.timeZone = timeZone 232 | self.location = location 233 | self.date = date 234 | 235 | refresh() 236 | } 237 | 238 | init(location: CLLocation, timeZone: Double, date: Date = Date()) { 239 | let timeZoneSeconds: Int = Int(timeZone * SECONDS_IN_ONE_HOUR) 240 | self.timeZone = TimeZone.init(secondsFromGMT: timeZoneSeconds) ?? .current 241 | self.location = location 242 | self.date = date 243 | 244 | refresh() 245 | } 246 | 247 | /*-------------------------------------------------------------------- 248 | Public methods 249 | *-------------------------------------------------------------------*/ 250 | 251 | /*-------------------------------------------------------------------- 252 | Changing date of interest 253 | *-------------------------------------------------------------------*/ 254 | 255 | public mutating func setDate(_ newDate: Date) { 256 | let newDay = calendar.dateComponents([.day,.month,.year], from: newDate) 257 | let oldDay = calendar.dateComponents([.day,.month,.year], from: date) 258 | 259 | let isSameDay: Bool = (newDay == oldDay) 260 | date = newDate 261 | 262 | refresh(needToComputeSunEvents: !isSameDay) //If is the same day no need to compute again Daily Sun Events 263 | } 264 | 265 | /*-------------------------------------------------------------------- 266 | Changing Location 267 | *-------------------------------------------------------------------*/ 268 | 269 | 270 | /// Changing location and timezone 271 | /// - Parameters: 272 | /// - newLocation: New location 273 | /// - newTimeZone: New timezone for the given location. Is highly recommanded to pass a Timezone initialized via .init(identifier: ) method 274 | public mutating func setLocation(_ newLocation: CLLocation,_ newTimeZone: TimeZone) { 275 | timeZone = newTimeZone 276 | location = newLocation 277 | refresh() 278 | } 279 | 280 | /// Changing only the location 281 | /// - Parameter newLocation: New Location 282 | public mutating func setLocation(_ newLocation: CLLocation) { 283 | location = newLocation 284 | refresh() 285 | } 286 | 287 | 288 | /// Is highly recommanded to use the other method to change both location and timezone. This will be kept only for backwards retrocompatibility. 289 | /// - Parameters: 290 | /// - newLocation: New Location 291 | /// - newTimeZone: New Timezone express in Double. For timezones which differs of half an hour add 0.5, 292 | public mutating func setLocation(_ newLocation: CLLocation,_ newTimeZone: Double) { 293 | let timeZoneSeconds: Int = Int(newTimeZone * SECONDS_IN_ONE_HOUR) 294 | timeZone = TimeZone(secondsFromGMT: timeZoneSeconds) ?? .current 295 | location = newLocation 296 | refresh() 297 | } 298 | 299 | 300 | /*-------------------------------------------------------------------- 301 | Changing Timezone 302 | *-------------------------------------------------------------------*/ 303 | 304 | /// Changing only the timezone. 305 | /// - Parameter newTimeZone: New Timezone 306 | public mutating func setTimeZone(_ newTimeZone: TimeZone) { 307 | timeZone = newTimeZone 308 | refresh() 309 | } 310 | 311 | /// Is highly recommanded to use the other method to change timezone. This will be kept only for backwards retrocompatibility. 312 | /// - Parameter newTimeZone: New Timezone express in Double. For timezones which differs of half an hour add 0.5, 313 | public mutating func setTimeZone(_ newTimeZone: Double) { 314 | let timeZoneSeconds: Int = Int(newTimeZone * SECONDS_IN_ONE_HOUR) 315 | timeZone = TimeZone(secondsFromGMT: timeZoneSeconds) ?? .current 316 | refresh() 317 | } 318 | 319 | /*-------------------------------------------------------------------- 320 | Debug functions 321 | *-------------------------------------------------------------------*/ 322 | 323 | /// Dumps all the Sun Events dates 324 | public func dumpDateInfos(){ 325 | 326 | print("Current Date -> \(dateFormatter.string(from: date))") 327 | print("Sunrise -> \(dateFormatter.string(from: sunrise))") 328 | print("Sunset -> \(dateFormatter.string(from: sunset))") 329 | print("Solar Noon -> \(dateFormatter.string(from: solarNoon))") 330 | print("Solar Midnight -> \(dateFormatter.string(from: solarMidnight))") 331 | print("Evening Golden Hour Start -> \(dateFormatter.string(from: eveningGoldenHourStart))") 332 | print("Evening Golden Hour End -> \(dateFormatter.string(from: eveningGoldenHourEnd))") 333 | print("Morning Golden Hour Start -> \(dateFormatter.string(from: morningGoldenHourStart))") 334 | print("Morning Golden Hour End -> \(dateFormatter.string(from: morningGoldenHourEnd))") 335 | print("Civil dusk -> \(dateFormatter.string(from: civilDusk))") 336 | print("Civil Dawn -> \(dateFormatter.string(from: civilDawn))") 337 | print("Nautical Dusk -> \(dateFormatter.string(from: nauticalDusk))") 338 | print("Nautical Dawn -> \(dateFormatter.string(from: nauticalDawn))") 339 | print("Astronomical Dusk -> \(dateFormatter.string(from: astronomicalDusk))") 340 | print("Astronomical Dawn -> \(dateFormatter.string(from: astronomicalDawn))") 341 | print("Morning Blue Hour Start -> \(dateFormatter.string(from: morningBlueHourStart))") 342 | print("Morning Blue Hour End -> \(dateFormatter.string(from: morningBlueHourEnd))") 343 | print("evening Blue Hour Start -> \(dateFormatter.string(from: eveningBlueHourStart))") 344 | print("evening Blue Hour End -> \(dateFormatter.string(from: eveningBlueHourEnd))") 345 | 346 | print("March Equinox -> \(dateFormatter.string(from: marchEquinox))") 347 | print("June Solstice -> \(dateFormatter.string(from: juneSolstice))") 348 | print("September Equinox -> \(dateFormatter.string(from: septemberEquinox))") 349 | print("December Solstice -> \(dateFormatter.string(from: decemberSolstice))") 350 | } 351 | 352 | /*-------------------------------------------------------------------- 353 | Private Variables 354 | *-------------------------------------------------------------------*/ 355 | 356 | private var calendar: Calendar { 357 | var calendar: Calendar = .init(identifier: .gregorian) 358 | calendar.timeZone = self.timeZone 359 | 360 | return calendar 361 | } 362 | 363 | private var dateFormatter: DateFormatter { 364 | let dateFormatter = DateFormatter() 365 | dateFormatter.locale = .current 366 | dateFormatter.timeZone = self.timeZone 367 | dateFormatter.timeStyle = .full 368 | dateFormatter.dateStyle = .full 369 | return dateFormatter 370 | } 371 | 372 | private var timeZoneInSeconds: Int { 373 | timeZone.secondsFromGMT(for: self.date) 374 | } 375 | 376 | private var sunHorizonCoordinates: HorizonCoordinates = .init(altitude: .zero, azimuth: .zero) 377 | 378 | 379 | //Sun constants 380 | private let sunEclipticLongitudeAtTheEpoch: Angle = .init(degrees: 280.466069) 381 | private let sunEclipticLongitudePerigee: Angle = .init(degrees: 282.938346) 382 | 383 | /// Number of the days passed since the start of the year for the self.date 384 | private var daysPassedFromStartOfTheYear: Int { 385 | let year = calendar.component(.year, from: date) 386 | 387 | let dateFormatter: DateFormatter = DateFormatter() 388 | 389 | dateFormatter.dateFormat = "yyyy/mm/dd" 390 | dateFormatter.calendar = calendar 391 | let dataFormatted = dateFormatter.date(from: "\(year)/01/01") 392 | 393 | let startOfYear = calendar.startOfDay(for: dataFormatted!) 394 | let startOfDay = calendar.startOfDay(for: date) 395 | 396 | var daysPassedFromStartOfTheYear = calendar.dateComponents([.day], from: startOfYear, to: startOfDay).day! 397 | daysPassedFromStartOfTheYear = daysPassedFromStartOfTheYear + 1 398 | 399 | return daysPassedFromStartOfTheYear 400 | } 401 | 402 | private var b: Angle { 403 | let angleInDegrees: Double = (360 / 365 * Double(daysPassedFromStartOfTheYear - 81)) 404 | 405 | return .degrees(angleInDegrees) 406 | } 407 | 408 | private var equationOfTime: Double { 409 | let bRad = b.radians 410 | 411 | return 9.87 * sin(2 * bRad) - 7.53 * cos(bRad) - 1.5 * sin(bRad) 412 | } 413 | 414 | private var localStandardTimeMeridian: Double { 415 | return (Double(self.timeZoneInSeconds) / SECONDS_IN_ONE_HOUR) * 15 //TimeZone in hour 416 | } 417 | 418 | private var timeCorrectionFactorInSeconds: Double { 419 | let timeCorrectionFactor = 4 * (location.coordinate.longitude - localStandardTimeMeridian) + equationOfTime 420 | let minutes: Double = Double(Int(timeCorrectionFactor) * 60) 421 | let seconds: Double = timeCorrectionFactor.truncatingRemainder(dividingBy: 1) * 100 422 | let timeCorrectionFactorInSeconds = minutes + seconds 423 | 424 | return timeCorrectionFactorInSeconds 425 | } 426 | 427 | 428 | /*-------------------------------------------------------------------- 429 | Private methods 430 | *-------------------------------------------------------------------*/ 431 | 432 | /// Updates in order all the sun coordinates: horizon, ecliptic and equatorial. 433 | /// Then get rise, set and noon times and their relative azimuths in degrees. 434 | /// Compute Solar noon. 435 | /// Compute Golden hour start and end time. 436 | /// Compute civil dusk and Civil Dawn time 437 | /// 438 | /// - Parameter needToComputeAgainSunEvents: True if Sunrise,Sunset and all the others daily sun events have to be computed. 439 | private mutating func refresh(needToComputeSunEvents: Bool = true) { 440 | updateSunCoordinates() 441 | 442 | if(needToComputeSunEvents){ 443 | self.sunrise = getSunrise() ?? Date() 444 | self.sunriseAzimuth = getSunHorizonCoordinatesFrom(date: sunrise).azimuth.degrees 445 | self.sunset = getSunset() ?? Date() 446 | self.sunsetAzimuth = getSunHorizonCoordinatesFrom(date: sunset).azimuth.degrees 447 | self.solarNoon = getSolarNoon() ?? Date() 448 | self.solarMidnight = getSolarMidnight() ?? Date() 449 | self.solarNoonAzimuth = getSunHorizonCoordinatesFrom(date: solarNoon).azimuth.degrees 450 | self.eveningGoldenHourStart = getEveningGoldenHourStart() ?? Date() 451 | self.eveningGoldenHourEnd = getEveningGoldenHourEnd() ?? Date() 452 | self.civilDusk = getCivilDusk() ?? Date() 453 | self.civilDawn = getCivilDawn() ?? Date() 454 | self.nauticalDusk = getNauticalDusk() ?? Date() 455 | self.nauticalDawn = getNauticalDawn() ?? Date() 456 | self.astronomicalDusk = getAstronomicalDusk() ?? Date() 457 | self.astronomicalDawn = getAstronomicalDawn() ?? Date() 458 | self.morningGoldenHourStart = getMorningGoldenHourStart() ?? Date() 459 | self.morningGoldenHourEnd = getMorningGoldenHourEnd() ?? Date() 460 | 461 | } 462 | 463 | self.marchEquinox = getMarchEquinox() ?? Date() 464 | self.juneSolstice = getJuneSolstice() ?? Date() 465 | self.septemberEquinox = getSeptemberEquinox() ?? Date() 466 | self.decemberSolstice = getDecemberSolstice() ?? Date() 467 | } 468 | 469 | private func getSunMeanAnomaly(from elapsedDaysSinceStandardEpoch: Double) -> Angle { 470 | //Compute mean anomaly sun 471 | var sunMeanAnomaly: Angle = .init(degrees:(((360.0 * elapsedDaysSinceStandardEpoch) / 365.242191) + sunEclipticLongitudeAtTheEpoch.degrees - sunEclipticLongitudePerigee.degrees)) 472 | sunMeanAnomaly = .init(degrees: extendedMod(sunMeanAnomaly.degrees, 360)) 473 | 474 | return sunMeanAnomaly 475 | } 476 | 477 | private func getSunEclipticLongitude(from sunMeanAnomaly: Angle) -> Angle { 478 | //eclipticLatitude 479 | let equationOfCenter = 360 / Double.pi * sin(sunMeanAnomaly.radians) * 0.016708 480 | let trueAnomaly = sunMeanAnomaly.degrees + equationOfCenter 481 | var eclipticLatitude: Angle = .init(degrees: trueAnomaly + sunEclipticLongitudePerigee.degrees) 482 | 483 | if eclipticLatitude.degrees > 360 { 484 | eclipticLatitude.degrees -= 360 485 | } 486 | 487 | return eclipticLatitude 488 | } 489 | 490 | 491 | /// Updates Horizon coordinates, Ecliptic coordinates and Equatorial coordinates of the Sun 492 | private mutating func updateSunCoordinates() { 493 | //Step1: 494 | //Convert LCT to UT, GST, and LST times and adjust the date if needed 495 | let gstHMS = uT2GST(self.date) 496 | let lstHMS = gST2LST(gstHMS,longitude: longitude) 497 | 498 | let lstDecimal = lstHMS.hMS2Decimal() 499 | 500 | //Step2: 501 | //Julian number for standard epoch 2000 502 | let jdEpoch = 2451545.00 503 | 504 | //Step3: 505 | //Compute the Julian day number for the desired date using the Greenwich date and TT 506 | 507 | let jdTT = jdFromDate(date: self.date) 508 | 509 | //Step5: 510 | //Compute the total number of elapsed days, including fractional days, since the standard epoch (i.e., JD − JDe) 511 | let elapsedDaysSinceStandardEpoch: Double = jdTT - jdEpoch //De 512 | 513 | //Step6: Use the algorithm from section 6.2.3 to calculate the Sun’s ecliptic longitude and mean anomaly for the given UT date and time. 514 | let sunMeanAnomaly = getSunMeanAnomaly(from: elapsedDaysSinceStandardEpoch) 515 | 516 | //Step7: Use Equation 6.2.4 to aproximate the Equation of the center 517 | let equationOfCenter = 360 / Double.pi * sin(sunMeanAnomaly.radians) * 0.016708 518 | 519 | //Step8: Add EoC to sun mean anomaly to get the sun true anomaly 520 | var sunTrueAnomaly = sunMeanAnomaly.degrees + equationOfCenter 521 | 522 | //Step9: 523 | sunTrueAnomaly = extendedMod(sunTrueAnomaly, 360) 524 | 525 | //Step10: 526 | var sunEclipticLongitude: Angle = .init(degrees: sunTrueAnomaly + sunEclipticLongitudePerigee.degrees) 527 | 528 | //Step11: 529 | if sunEclipticLongitude.degrees > 360 { 530 | sunEclipticLongitude.degrees -= 360 531 | } 532 | 533 | sunEclipticCoordinates = .init(eclipticLatitude: .zero, eclipticLongitude: sunEclipticLongitude) 534 | 535 | //Step12: Ecliptic to Equatorial 536 | sunEquatorialCoordinates = sunEclipticCoordinates.ecliptic2Equatorial() 537 | 538 | //Step13: Equatorial to Horizon 539 | sunHorizonCoordinates = sunEquatorialCoordinates.equatorial2Horizon(lstDecimal: lstDecimal,latitude: latitude) ?? .init(altitude: .zero, azimuth: .zero) 540 | 541 | } 542 | 543 | public func getSunHorizonCoordinatesFrom(date: Date) -> HorizonCoordinates { 544 | //Step1: 545 | //Convert LCT to UT, GST, and LST times and adjust the date if needed 546 | let gstHMS = uT2GST(date) 547 | let lstHMS = gST2LST(gstHMS,longitude: longitude) 548 | 549 | let lstDecimal = lstHMS.hMS2Decimal() 550 | 551 | //Step2: 552 | //Julian number for standard epoch 2000 553 | let jdEpoch = 2451545.00 554 | 555 | //Step3: 556 | //Compute the Julian day number for the desired date using the Greenwich date and TT 557 | 558 | let jdTT = jdFromDate(date: date) 559 | 560 | //Step5: 561 | //Compute the total number of elapsed days, including fractional days, since the standard epoch (i.e., JD − JDe) 562 | let elapsedDaysSinceStandardEpoch: Double = jdTT - jdEpoch //De 563 | 564 | //Step6: Use the algorithm from section 6.2.3 to calculate the Sun’s ecliptic longitude and mean anomaly for the given UT date and time. 565 | let sunMeanAnomaly = getSunMeanAnomaly(from: elapsedDaysSinceStandardEpoch) 566 | 567 | //Step7: Use Equation 6.2.4 to aproximate the Equation of the center 568 | let equationOfCenter = 360 / Double.pi * sin(sunMeanAnomaly.radians) * 0.016708 569 | 570 | //Step8: Add EoC to sun mean anomaly to get the sun true anomaly 571 | var sunTrueAnomaly = sunMeanAnomaly.degrees + equationOfCenter 572 | 573 | //Step9: Add or subtract multiples of 360° to adjust sun true anomaly to the range of 0° to 360° 574 | sunTrueAnomaly = extendedMod(sunTrueAnomaly, 360) 575 | 576 | 577 | //Step10: Getting ecliptic longitude. 578 | var sunEclipticLongitude: Angle = .init(degrees: sunTrueAnomaly + sunEclipticLongitudePerigee.degrees) 579 | 580 | //Step11: 581 | if sunEclipticLongitude.degrees > 360 { 582 | sunEclipticLongitude.degrees -= 360 583 | } 584 | 585 | let sunEclipticCoordinates: EclipticCoordinates = .init(eclipticLatitude: .zero, eclipticLongitude: sunEclipticLongitude) 586 | 587 | //Step12: Ecliptic to Equatorial 588 | var sunEquatorialCoordinates: EquatorialCoordinates = sunEclipticCoordinates.ecliptic2Equatorial() 589 | 590 | //Step13: Equatorial to Horizon 591 | let sunHorizonCoordinates: HorizonCoordinates = sunEquatorialCoordinates.equatorial2Horizon(lstDecimal: lstDecimal,latitude: latitude) ?? .init(altitude: .zero, azimuth: .zero) 592 | 593 | return .init(altitude: sunHorizonCoordinates.altitude, azimuth: sunHorizonCoordinates.azimuth) 594 | } 595 | 596 | /// Computes the solar noon for self.date. Solar noon is the time when the sun is highest in the sky. 597 | /// - Returns: Solar noon time 598 | private func getSolarNoon() -> Date? { 599 | let secondsForUTCSolarNoon = (720 - 4 * location.coordinate.longitude - equationOfTime) * 60 600 | let secondsForSolarNoon = secondsForUTCSolarNoon + Double(timeZoneInSeconds) 601 | let startOfTheDay = calendar.startOfDay(for: date) 602 | 603 | let solarNoon = calendar.date(byAdding: .second, value: Int(secondsForSolarNoon) , to: startOfTheDay) 604 | 605 | return solarNoon 606 | } 607 | 608 | /// Computes the solar midnight for self.date. 609 | /// - Returns: Solar midnight time 610 | private func getSolarMidnight() -> Date? { 611 | 612 | let secondsForUTCSolarMidnight = (0 - 4 * location.coordinate.longitude - equationOfTime) * 60 613 | let secondsForSolarMidnight = secondsForUTCSolarMidnight + Double(timeZoneInSeconds) 614 | let startOfTheDay = calendar.startOfDay(for: date) 615 | 616 | let solarMidnight = calendar.date(byAdding: .second, value: Int(secondsForSolarMidnight) , to: startOfTheDay) 617 | 618 | return solarMidnight 619 | } 620 | 621 | 622 | 623 | /// Computes the Sunrise time for self.date 624 | /// - Returns: Sunrise time 625 | private func getSunrise() -> Date? { 626 | var haArg = (cos(Angle.degrees(90.833).radians)) / (cos(latitude.radians) * cos(sunEquatorialCoordinates.declination.radians)) - tan(latitude.radians) * tan(sunEquatorialCoordinates.declination.radians) 627 | 628 | haArg = clamp(lower: -1, upper: 1, number: haArg) 629 | let ha: Angle = .radians(acos(haArg)) 630 | let sunriseUTCMinutes = 720 - 4 * (location.coordinate.longitude + ha.degrees) - equationOfTime 631 | var sunriseSeconds = (Int(sunriseUTCMinutes) * 60) + timeZoneInSeconds 632 | let startOfDay = calendar.startOfDay(for: date) 633 | 634 | if sunriseSeconds < Int(SECONDS_IN_ONE_HOUR) { 635 | sunriseSeconds = 0 636 | } 637 | 638 | let hoursMinutesSeconds: (Int, Int, Int) = secondsToHoursMinutesSeconds(Int(sunriseSeconds)) 639 | 640 | let sunriseDate = calendar.date(bySettingHour: hoursMinutesSeconds.0, minute: hoursMinutesSeconds.1, second: hoursMinutesSeconds.2, of: startOfDay) 641 | 642 | return sunriseDate 643 | } 644 | 645 | /// Computes the Sunset time for self.date 646 | /// - Returns: Sunset time 647 | private func getSunset() -> Date? { 648 | var haArg = (cos(Angle.degrees(90.833).radians)) / (cos(latitude.radians) * cos(sunEquatorialCoordinates.declination.radians)) - tan(latitude.radians) * tan(sunEquatorialCoordinates.declination.radians) 649 | 650 | haArg = clamp(lower: -1, upper: 1, number: haArg) 651 | let ha: Angle = .radians(-acos(haArg)) 652 | let sunsetUTCMinutes = 720 - 4 * (location.coordinate.longitude + ha.degrees) - equationOfTime 653 | var sunsetSeconds = (Int(sunsetUTCMinutes) * 60) + timeZoneInSeconds 654 | let startOfDay = calendar.startOfDay(for: date) 655 | 656 | if sunsetSeconds > SECONDS_IN_ONE_DAY { 657 | sunsetSeconds = SECONDS_IN_ONE_DAY 658 | } 659 | 660 | let hoursMinutesSeconds: (Int, Int, Int) = secondsToHoursMinutesSeconds(Int(sunsetSeconds)) 661 | 662 | let sunsetDate = calendar.date(bySettingHour: hoursMinutesSeconds.0, minute: hoursMinutesSeconds.1, second: hoursMinutesSeconds.2, of: startOfDay) 663 | 664 | return sunsetDate 665 | } 666 | 667 | /// Computes the time at which the sun will reach the elevation given in input for self.date 668 | /// - Parameters: 669 | /// - elevation: Elevation 670 | /// - morning: Sun reaches a specific elevation twice, this boolean variable is needed to find out which one need to be considered. The one reached in the morning or not. 671 | /// - Returns: Time at which the Sun reaches that elevation. Nil if it didn't find it. 672 | private func getDateFrom(sunEvent : SunElevationEvents, morning: Bool = false) -> Date? { 673 | 674 | let elevationSun: Angle = .degrees(sunEvent.rawValue) 675 | var cosHra = (sin(elevationSun.radians) - sin(sunEquatorialCoordinates.declination.radians) * sin(latitude.radians)) / (cos(sunEquatorialCoordinates.declination.radians) * cos(latitude.radians)) 676 | cosHra = clamp(lower: -1, upper: 1, number: cosHra) 677 | let hraAngle: Angle = .radians(acos(cosHra)) 678 | var secondsForSunToReachElevation = (morning ? -1 : 1) * (hraAngle.degrees / 15) * SECONDS_IN_ONE_HOUR + TWELVE_HOUR_IN_SECONDS - timeCorrectionFactorInSeconds 679 | let startOfTheDay = calendar.startOfDay(for: date) 680 | 681 | if (Int(secondsForSunToReachElevation) > SECONDS_IN_ONE_DAY){ 682 | 683 | secondsForSunToReachElevation = Double(SECONDS_IN_ONE_DAY) 684 | } 685 | else if (secondsForSunToReachElevation < SECONDS_IN_ONE_HOUR){ 686 | 687 | secondsForSunToReachElevation = 0 688 | } 689 | let hoursMinutesSeconds: (Int, Int, Int) = secondsToHoursMinutesSeconds(Int(secondsForSunToReachElevation)) 690 | 691 | let newDate = calendar.date(bySettingHour: hoursMinutesSeconds.0 , minute: hoursMinutesSeconds.1, second: hoursMinutesSeconds.2, of: startOfTheDay) 692 | 693 | 694 | return newDate 695 | } 696 | 697 | 698 | 699 | /// Golden Hour in the evening begins when the sun reaches elevation equals to 6 degrees 700 | /// - Returns: Time at which the GoldenHour starts 701 | private func getEveningGoldenHourStart() -> Date? { 702 | guard let eveningGoldenHourStart = getDateFrom(sunEvent: .eveningGoldenHourStart) else { 703 | return nil 704 | } 705 | 706 | return eveningGoldenHourStart 707 | } 708 | 709 | /// Golden Hour in the evening ends when the sun reaches elevation equals to -4 degrees 710 | /// - Returns: Time at which the GoldenHour ends 711 | private func getEveningGoldenHourEnd() -> Date? { 712 | guard let goldenHourFinish = getDateFrom(sunEvent: .eveningGoldenHourEnd) else { 713 | return nil 714 | } 715 | 716 | return goldenHourFinish 717 | } 718 | 719 | /// Civil Dawn is when the Sun reaches -6 degrees of elevation. Also known as civil sunrise 720 | /// - Returns: Civil Dawn time 721 | private func getCivilDawn() -> Date? { 722 | guard let civilDawn = getDateFrom(sunEvent: .civil,morning: true) else { 723 | return nil 724 | } 725 | return civilDawn 726 | } 727 | 728 | /// civil dusk is when the Sun reaches -6 degrees of elevation. Also known as civil sunrise. 729 | /// - Returns: civil dusk time 730 | private func getCivilDusk() -> Date? { 731 | guard let civilDusk = getDateFrom(sunEvent: .civil, morning: false) else { 732 | return nil 733 | } 734 | return civilDusk 735 | } 736 | 737 | /// Nautical Dusk is when the Sun reaches -12 degrees of elevation. 738 | /// - Returns: Nautical Dusk 739 | private func getNauticalDusk() -> Date? { 740 | guard let nauticalDusk = getDateFrom(sunEvent: .nautical, morning: false) else { 741 | return nil 742 | } 743 | return nauticalDusk 744 | } 745 | 746 | /// Nautical Dusk is when the Sun reaches -12 degrees of elevation. 747 | /// - Returns: Nautical Dawn 748 | private func getNauticalDawn() -> Date? { 749 | guard let nauticalDawn = getDateFrom(sunEvent: .nautical, morning: true) else { 750 | return nil 751 | } 752 | return nauticalDawn 753 | } 754 | 755 | /// Astronomical Dusk is when the Sun reaches -18 degrees of elevation. 756 | /// - Returns: Astronomical Dusk 757 | private func getAstronomicalDusk() -> Date? { 758 | guard let astronomicalDusk = getDateFrom(sunEvent: .astronomical, morning: false) else { 759 | return nil 760 | } 761 | return astronomicalDusk 762 | } 763 | 764 | /// Astronomical Dawn is when the Sun reaches -18 degrees of elevation. 765 | /// - Returns: Astronomical Dawn 766 | private func getAstronomicalDawn() -> Date? { 767 | guard let astronomicalDawn = getDateFrom(sunEvent: .astronomical, morning: true) else { 768 | return nil 769 | } 770 | return astronomicalDawn 771 | } 772 | 773 | /// Morning Golden Hour start when Sun reaches -4 degress of elevation 774 | /// - Returns: Morning golden hour start 775 | private func getMorningGoldenHourStart() -> Date? { 776 | guard let morningGoldenHourStart = getDateFrom(sunEvent: .morningGoldenHourStart , morning: true) else { 777 | return nil 778 | } 779 | return morningGoldenHourStart 780 | } 781 | 782 | /// Morning Golden Hour ends when Sun reaches 6 degress of elevation 783 | /// - Returns: Morning golden hour end 784 | private func getMorningGoldenHourEnd() -> Date? { 785 | guard let morningGoldenHourEnd = getDateFrom(sunEvent: .morningGoldenHourEnd , morning: true) else { 786 | return nil 787 | } 788 | return morningGoldenHourEnd 789 | } 790 | 791 | private func getMarchEquinox() -> Date? { 792 | 793 | let year = Double(calendar.component(.year, from: self.date)) 794 | let t: Double = year / 1000 795 | let julianDayMarchEquinox: Double = 1721139.2855 + 365.2421376 * year + 0.0679190 * pow(t, 2) - 0.0027879 * pow(t, 3) 796 | 797 | let marchEquinoxUTC = dateFromJd(jd: julianDayMarchEquinox) 798 | 799 | return marchEquinoxUTC 800 | } 801 | 802 | private func getJuneSolstice() -> Date? { 803 | 804 | let year = Double(calendar.component(.year, from: self.date)) 805 | let t: Double = year / 1000 806 | let julianDayJuneSolstice: Double = 1721233.2486 + 365.2417284 * year - 0.0530180 * pow(t, 2) + 0.0093320 * pow(t, 3) 807 | 808 | let juneSolsticeUTC = dateFromJd(jd: julianDayJuneSolstice) 809 | 810 | return juneSolsticeUTC 811 | } 812 | 813 | private func getSeptemberEquinox() -> Date? { 814 | 815 | let year = Double(calendar.component(.year, from: self.date)) 816 | let t: Double = year / 1000 817 | let julianDaySeptemberEquinox: Double = 1721325.6978 + 365.2425055 * year - 0.126689 * pow(t, 2) + 0.0019401 * pow(t, 3) 818 | 819 | let septemberEquinoxUTC = dateFromJd(jd: julianDaySeptemberEquinox) 820 | 821 | return septemberEquinoxUTC 822 | } 823 | 824 | private func getDecemberSolstice() -> Date? { 825 | 826 | let year = Double(calendar.component(.year, from: self.date)) 827 | let t: Double = year / 1000 828 | let julianDayDecemberSolstice: Double = 1721414.3920 + 365.2428898 * year - 0.0109650 * pow(t, 2) - 0.0084885 * pow(t, 3) 829 | 830 | let decemberSolsticeUTC = dateFromJd(jd: julianDayDecemberSolstice) 831 | 832 | return decemberSolsticeUTC 833 | } 834 | 835 | 836 | /// - Returns: Length in meters of the object's shadow by the provided object height and current sun altitude. 837 | public func shadowLength(for objectHeight: Double = 1, with altitude: Angle? = nil) -> Double? { 838 | let altitude = altitude ?? self.altitude 839 | 840 | return if altitude.degrees > 0 && altitude.degrees < 90 { 841 | objectHeight / tan(altitude.radians) 842 | } else if altitude.degrees <= 0 { 843 | nil 844 | } else { 845 | 0 846 | } 847 | } 848 | } 849 | 850 | extension Sun: Equatable { 851 | public static func == (lhs: Sun, rhs: Sun) -> Bool { 852 | lhs.location == rhs.location && 853 | lhs.timeZone == rhs.timeZone && 854 | lhs.date == rhs.date 855 | } 856 | } 857 | 858 | extension Sun: Hashable { 859 | public func hash(into hasher: inout Hasher) { 860 | hasher.combine(location) 861 | hasher.combine(timeZone) 862 | hasher.combine(date) 863 | } 864 | } 865 | --------------------------------------------------------------------------------