diff --git a/Sources/SunKit/Angle.swift b/Sources/SunKit/Angle.swift index ec02601..8a89b04 100644 --- a/Sources/SunKit/Angle.swift +++ b/Sources/SunKit/Angle.swift @@ -16,11 +16,10 @@ // See the License for the specific language governing permissions and // limitations under the License. - import Foundation + public struct Angle: Equatable, Hashable, Codable, Sendable { - public static var zero: Angle = .init() public init() { @@ -30,11 +29,11 @@ public struct Angle: Equatable, Hashable, Codable, Sendable { public init(radians: Double) { _radians = radians } - + public init(degrees: Double) { _radians = degrees * Double.pi / 180.0 } - + public var degrees: Double { get { _radians * 180.0 / Double.pi } set { _radians = newValue * Double.pi / 180 } @@ -54,5 +53,4 @@ public struct Angle: Equatable, Hashable, Codable, Sendable { public static func radians(_ value: Double) -> Angle { .init(radians: value) } - } diff --git a/Sources/SunKit/DMS.swift b/Sources/SunKit/DMS.swift index 7948f59..f5d9471 100644 --- a/Sources/SunKit/DMS.swift +++ b/Sources/SunKit/DMS.swift @@ -18,16 +18,20 @@ import Foundation + /// DMS format to express angles public struct DMS: Equatable, Hashable, Codable, Sendable { - public var degrees: Double public var minutes: Double public var seconds: Double public var isANegativeZero: Bool - - init(degrees: Double, minutes: Double, seconds: Double, isANegativeZero: Bool = false) { + init( + degrees: Double, + minutes: Double, + seconds: Double, + isANegativeZero: Bool = false + ) { self.degrees = degrees self.minutes = minutes self.seconds = seconds @@ -36,8 +40,7 @@ public struct DMS: Equatable, Hashable, Codable, Sendable { ///From decimal it will create the corresponding angle in DMS format /// - Parameter decimal: Decimal angle that will be converted in DMS format - public init(decimal: Double){ - + public init(decimal: Double) { //Step1: let sign = decimal < 0 ? -1 : 1 //Step2: @@ -50,26 +53,23 @@ public struct DMS: Equatable, Hashable, Codable, Sendable { let seconds = 60 * (60 * dec.truncatingRemainder(dividingBy: 1)).truncatingRemainder(dividingBy: 1) //Step6: degrees *= sign + if degrees == 0 && sign == -1 { self.degrees = Double(degrees) self.minutes = Double(minutes) self.seconds = seconds self.isANegativeZero = true - } - else{ + } else { self.degrees = Double(degrees) self.minutes = Double(minutes) self.seconds = seconds self.isANegativeZero = false } - - } /// It converts from DMS format to decimal /// - Returns: DMS of the instance expressed in decimal format public func dMS2Decimal() -> Double { - //Step1: let sign: Double = degrees < 0 ? -1 : 1 //Step2: @@ -86,7 +86,6 @@ public struct DMS: Equatable, Hashable, Codable, Sendable { decimal *= sign if isANegativeZero{ - decimal *= -1 } diff --git a/Sources/SunKit/Extensions/Calendar+startOfYear.swift b/Sources/SunKit/Extensions/Calendar+startOfYear.swift new file mode 100644 index 0000000..449300d --- /dev/null +++ b/Sources/SunKit/Extensions/Calendar+startOfYear.swift @@ -0,0 +1,34 @@ +// +// Calendar+startOfYear.swift +// +// +// Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + + +extension Calendar { + func numberOfDaysSinceStartOfTheYear(for date: Date) -> Int { + let startOfTheYear: Date = startOfYear(date) + let startOfTheDay = startOfDay(for: date) + let numberOfDays = dateComponents([.day], from: startOfTheYear, to: startOfTheDay) + + return numberOfDays.day! + 1 + } + + func startOfYear(_ date: Date) -> Date { + return self.date(from: self.dateComponents([.year], from: date))! + } +} diff --git a/Sources/SunKit/Extensions.swift b/Sources/SunKit/Extensions/Date+Strideable.swift similarity index 51% rename from Sources/SunKit/Extensions.swift rename to Sources/SunKit/Extensions/Date+Strideable.swift index bb574c3..a6b6d5d 100644 --- a/Sources/SunKit/Extensions.swift +++ b/Sources/SunKit/Extensions/Date+Strideable.swift @@ -1,6 +1,6 @@ // -// Extensions.swift -// +// Date+Strideable.swift +// // // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano // @@ -18,7 +18,8 @@ import Foundation -//It consents us too loop between two dates for n as interval time + +// Allows us to loop between two dates using interval time n. extension Date: @retroactive Strideable { public func distance(to other: Date) -> TimeInterval { return other.timeIntervalSinceReferenceDate - self.timeIntervalSinceReferenceDate @@ -27,10 +28,13 @@ extension Date: @retroactive Strideable { func toString(_ timeZone: TimeZone) -> String { let df = DateFormatter() df.timeZone = timeZone - let custom = DateFormatter.dateFormat(fromTemplate: "MMdd HH:mm", - options: 0, - locale: Locale(identifier: "en")) + let custom = DateFormatter.dateFormat( + fromTemplate: "MMdd HH:mm", + options: 0, + locale: Locale(identifier: "en") + ) df.dateFormat = custom + return df.string(from: self) } @@ -38,31 +42,3 @@ extension Date: @retroactive Strideable { return self + n } } - -extension Calendar { - func numberOfDaysSinceStartOfTheYear(for date: Date) -> Int { - let startOfTheYear: Date = startOfYear(date) - let startOfTheDay = startOfDay(for: date) - let numberOfDays = dateComponents([.day], from: startOfTheYear, to: startOfTheDay) - - return numberOfDays.day! + 1 - } - - func startOfYear(_ date: Date) -> Date { - return self.date(from: self.dateComponents([.year], from: date))! - } - -} - -extension TimeZone { - - func offset(_ date: Date) -> Double { - let res = - Int(self.secondsFromGMT(for: date)) - + Int(self.daylightSavingTimeOffset(for: date)) - - Int(Calendar.current.timeZone.secondsFromGMT(for: date)) - - Int(Calendar.current.timeZone.daylightSavingTimeOffset(for: date)) - return Double(res)/SECONDS_IN_ONE_HOUR - - } -} diff --git a/Sources/SunKit/Extensions/TimeZone+offset.swift b/Sources/SunKit/Extensions/TimeZone+offset.swift new file mode 100644 index 0000000..b550ccf --- /dev/null +++ b/Sources/SunKit/Extensions/TimeZone+offset.swift @@ -0,0 +1,31 @@ +// +// TimeZone+offset.swift +// +// +// Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + + +extension TimeZone { + func offset(_ date: Date) -> Double { + let res = Int(self.secondsFromGMT(for: date)) + + Int(self.daylightSavingTimeOffset(for: date)) + - Int(Calendar.current.timeZone.secondsFromGMT(for: date)) + - Int(Calendar.current.timeZone.daylightSavingTimeOffset(for: date)) + + return Double(res) / SECONDS_IN_ONE_HOUR + } +} diff --git a/Sources/SunKit/HMS.swift b/Sources/SunKit/HMS.swift index 85c6c46..50f6b9b 100644 --- a/Sources/SunKit/HMS.swift +++ b/Sources/SunKit/HMS.swift @@ -1,6 +1,6 @@ // // HMS.swift -// +// // // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano // @@ -21,13 +21,11 @@ import Foundation /// Time expressed in HMS format public struct HMS: Equatable, Hashable, Codable, Sendable { - public var hours: Double public var minutes: Double public var seconds: Double public init(from date: Date){ - var calendar: Calendar = .init(identifier: .gregorian) calendar.timeZone = .init(abbreviation: "GMT")! @@ -37,14 +35,12 @@ public struct HMS: Equatable, Hashable, Codable, Sendable { } public init(hours: Double,minutes: Double,seconds: Double){ - self.hours = hours self.minutes = minutes self.seconds = seconds } public init(decimal: Double){ - //Step1: let sign = decimal < 0 ? -1 : 1 //Step2: @@ -57,17 +53,16 @@ public struct HMS: Equatable, Hashable, Codable, Sendable { let seconds = 60 * (60 * dec.truncatingRemainder(dividingBy: 1)).truncatingRemainder(dividingBy: 1) //Step6: hours *= sign - + self.hours = Double(hours) self.minutes = Double(minutes) self.seconds = seconds - + } /// It converts from HMS format to decimal /// - Returns: HMS of the instance expressed in decimal format public func hMS2Decimal() -> Double { - //Step3: let dm: Double = Double(seconds / 60) //Step4: @@ -79,5 +74,4 @@ public struct HMS: Equatable, Hashable, Codable, Sendable { return decimalHour } - } diff --git a/Sources/SunKit/EclipticCoordinates.swift b/Sources/SunKit/Sun Coordinates/EclipticCoordinates.swift similarity index 86% rename from Sources/SunKit/EclipticCoordinates.swift rename to Sources/SunKit/Sun Coordinates/EclipticCoordinates.swift index b0c627e..b0dba98 100644 --- a/Sources/SunKit/EclipticCoordinates.swift +++ b/Sources/SunKit/Sun Coordinates/EclipticCoordinates.swift @@ -20,7 +20,6 @@ import Foundation public struct EclipticCoordinates: Equatable, Hashable, Codable, Sendable { - public static let obliquityOfTheEcliptic: Angle = .init(degrees: 23.439292) public var eclipticLatitude: Angle //beta @@ -28,25 +27,14 @@ public struct EclipticCoordinates: Equatable, Hashable, Codable, Sendable { /// Converts ecliptic coordinatates to equatorial coordinates /// - Returns: Equatorial coordinates of the instance - public func ecliptic2Equatorial() -> EquatorialCoordinates{ - - //Step4: + public func ecliptic2Equatorial() -> EquatorialCoordinates { let tEclipticToEquatorial: Angle = .init(radians: sin(eclipticLatitude.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) + cos(eclipticLatitude.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians) * sin(eclipticLongitude.radians)) - - //Step5: let moonDeclination: Angle = .init(radians: asin(tEclipticToEquatorial.radians)) - - //Step6: let yEclipticToEquatorial = sin(eclipticLongitude.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) - tan(eclipticLatitude.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians) - - //Step7: let xEclipticToEquatorial = cos(eclipticLongitude.radians) - - //Step8: var r: Angle = .init(radians: atan(yEclipticToEquatorial / xEclipticToEquatorial)) - //Step9: - switch (yEclipticToEquatorial >= 0,xEclipticToEquatorial >= 0){ + switch (yEclipticToEquatorial >= 0, xEclipticToEquatorial >= 0) { case (true, true): break @@ -58,7 +46,7 @@ public struct EclipticCoordinates: Equatable, Hashable, Codable, Sendable { r.degrees += 180 } - let alfa: Angle = .init(degrees: r.degrees / 15) + let alfa: Angle = .init(degrees: r.degrees / 15) let delta: Angle = .init(degrees: moonDeclination.degrees) return .init(declination: delta, rightAscension: alfa) diff --git a/Sources/SunKit/EquatorialCoordinates.swift b/Sources/SunKit/Sun Coordinates/EquatorialCoordinates.swift similarity index 70% rename from Sources/SunKit/EquatorialCoordinates.swift rename to Sources/SunKit/Sun Coordinates/EquatorialCoordinates.swift index 7824388..262df49 100644 --- a/Sources/SunKit/EquatorialCoordinates.swift +++ b/Sources/SunKit/Sun Coordinates/EquatorialCoordinates.swift @@ -1,6 +1,6 @@ // // EquatorialCoordinates.swift -// +// // // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano // @@ -18,66 +18,64 @@ import Foundation + public struct EquatorialCoordinates: Equatable, Hashable, Codable, Sendable { - public private(set) var rightAscension: Angle? // rightAscension.degrees refers to h format public private(set) var declination: Angle //delta private(set) var hourAngle: Angle? - init(declination: Angle,rightAscension: Angle, hourAngle: Angle){ + init(declination: Angle, rightAscension: Angle, hourAngle: Angle) { self.declination = declination self.rightAscension = rightAscension self.hourAngle = hourAngle } - init(declination: Angle,rightAscension: Angle){ + init(declination: Angle, rightAscension: Angle) { self.declination = declination self.rightAscension = rightAscension self.hourAngle = nil } - init(declination: Angle,hourAngle: Angle){ + init(declination: Angle, hourAngle: Angle) { self.declination = declination self.hourAngle = hourAngle self.rightAscension = nil } - init(declination: Angle){ + init(declination: Angle) { self.declination = declination } - /// To set right ascension we need LST and right ascension. If right ascension is nill we can't set it. - /// - Parameter lstDecimal: Local Sideral Time in decimal - /// - Returns: The value of hour angle just been set. Nil if right ascension is also nil - public mutating func setHourAngleFrom(lstDecimal: Double) -> Angle?{ + public func equatorial2Ecliptic() -> EclipticCoordinates? { + guard var rightAscension = rightAscension else { + return nil + } - guard let rightAscension = self.rightAscension else {return nil} - - var hourAngleDecimal = lstDecimal - rightAscension.degrees - if hourAngleDecimal < 0 { - hourAngleDecimal += 24 + rightAscension.degrees = rightAscension.degrees * 15 // from h format to degrees + + let tEquatorialToEcliptic: Angle = .init(radians: sin(declination.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) - cos(declination.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians) * sin(rightAscension.radians)) + let eclipticLatitude: Angle = .init(radians: asin(tEquatorialToEcliptic.radians)) + let yEquatorialToEcliptic = sin(rightAscension.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) + tan(declination.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians) + let xEquatorialToEcliptic = cos(rightAscension.radians) + var r: Angle = .init(radians: atan(yEquatorialToEcliptic / xEquatorialToEcliptic)) + + switch (yEquatorialToEcliptic >= 0, xEquatorialToEcliptic >= 0) { + case (true, true): + break + case (true,false): + r.degrees += 180 + case(false,true): + r.degrees += 360 + case(false,false): + r.degrees += 180 } - self.hourAngle = .init(degrees: hourAngleDecimal * 15) - - return self.hourAngle - } - - /// To set right ascension we need LST and hour angle. If hour angle is nill we can't set it. - /// - Parameter lstDecimal: Local Sideral Time in decimal - /// - Returns: The value of right ascension just been set. Nil if hour angle is also nil - public mutating func setRightAscensionFrom(lstDecimal: Double) -> Angle?{ - - guard let hourAngle = self.hourAngle else {return nil} - let hourAngleDecimal = hourAngle.degrees / 15 + let eclipticLongitude: Angle = .init(degrees: r.degrees) - self.rightAscension = .init(degrees: lstDecimal - hourAngleDecimal) - - return self.rightAscension - } - - + return .init(eclipticLatitude: eclipticLatitude, eclipticLongitude: eclipticLongitude) + } + /// Converts Equatorial coordinates to Horizon coordinates. /// /// Since horizon coordinates depend on the position, we need also latitude parameter to create an EquatorialCoordinates instance. @@ -86,25 +84,42 @@ public struct EquatorialCoordinates: Equatable, Hashable, Codable, Sendable { /// - lstDecimal: Local Sidereal Time in decimal format. /// - latitude: Latitude of the observer /// - Returns: Horizon coordinates for the given latitude and LST. Nil if hour angle cannot be computed due to the miss right ascnsion information - public mutating func equatorial2Horizon(lstDecimal: Double,latitude: Angle) -> HorizonCoordinates?{ + public mutating func equatorial2Horizon( + lstDecimal: Double, + latitude: Angle + ) -> HorizonCoordinates? { + guard let _ = setHourAngleFrom(lstDecimal: lstDecimal) else { + return nil + } - guard let _ = setHourAngleFrom(lstDecimal: lstDecimal) else {return nil} + return calculateHorizonCoordinates(from: latitude) + } + + /// To set right ascension we need LST and right ascension. If right ascension is nill we can't set it. + /// - Parameter lstDecimal: Local Sideral Time in decimal + /// - Returns: The value of hour angle just been set. Nil if right ascension is also nil + public mutating func setHourAngleFrom(lstDecimal: Double) -> Angle? { + guard let rightAscension = self.rightAscension else { + return nil + } - //Step4: - let tZeroEquatorialToHorizon = sin(declination.radians) * sin(latitude.radians) + cos(declination.radians) * cos(latitude.radians) * cos(hourAngle!.radians) + var hourAngleDecimal = lstDecimal - rightAscension.degrees + if hourAngleDecimal < 0 { + hourAngleDecimal += 24 + } + self.hourAngle = .init(degrees: hourAngleDecimal * 15) - //Step5: + return self.hourAngle + } + + private func calculateHorizonCoordinates(from latitude: Angle) -> HorizonCoordinates { + let tZeroEquatorialToHorizon = sin(declination.radians) * sin(latitude.radians) + cos(declination.radians) * cos(latitude.radians) * cos(hourAngle!.radians) let altitude: Angle = .init(radians: asin(tZeroEquatorialToHorizon)) - - //Step6: let tOneEquatorialToHorizon = sin(declination.radians) - sin(latitude.radians) * sin(altitude.radians) - - //Step7: let tTwoEquatorialToHorizon = tOneEquatorialToHorizon / (cos(latitude.radians) * cos(altitude.radians)) - - //Step8: var azimuth: Angle = .init(radians: acos(tTwoEquatorialToHorizon)) - if sin(hourAngle!.radians) >= 0{ + + if sin(hourAngle!.radians) >= 0 { azimuth.degrees = 360 - azimuth.degrees } @@ -118,67 +133,25 @@ public struct EquatorialCoordinates: Equatable, Hashable, Codable, Sendable { /// - Parameters: /// - latitude: Latitude of the observer /// - Returns: Horizon coordinates for the given latitude. Nil if hour angle is not defined. - public func equatorial2Horizon(latitude: Angle) -> HorizonCoordinates?{ - - guard let _ = self.hourAngle else {return nil} - - //Step4: - let tZeroEquatorialToHorizon = sin(declination.radians) * sin(latitude.radians) + cos(declination.radians) * cos(latitude.radians) * cos(hourAngle!.radians) - - //Step5: - let altitude: Angle = .init(radians: asin(tZeroEquatorialToHorizon)) - - //Step6: - let tOneEquatorialToHorizon = sin(declination.radians) - sin(latitude.radians) * sin(altitude.radians) - - //Step7: - let tTwoEquatorialToHorizon = tOneEquatorialToHorizon / (cos(latitude.radians) * cos(altitude.radians)) - - //Step8: - var azimuth: Angle = .init(radians: acos(tTwoEquatorialToHorizon)) - if sin(hourAngle!.radians) >= 0{ - azimuth.degrees = 360 - azimuth.degrees + public func equatorial2Horizon(latitude: Angle) -> HorizonCoordinates? { + guard let _ = self.hourAngle else { + return nil } - return .init(altitude: altitude, azimuth: azimuth) + return calculateHorizonCoordinates(from: latitude) } - public func equatorial2Ecliptic() -> EclipticCoordinates?{ - - guard var rightAscension = rightAscension else {return nil} - - rightAscension.degrees = rightAscension.degrees * 15 //from h format to degrees - - //Step5: - let tEquatorialToEcliptic: Angle = .init(radians: sin(declination.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) - cos(declination.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians) * sin(rightAscension.radians)) - - //Step6: - let eclipticLatitude: Angle = .init(radians: asin(tEquatorialToEcliptic.radians)) - - //Step7: - let yEquatorialToEcliptic = sin(rightAscension.radians) * cos(EclipticCoordinates.obliquityOfTheEcliptic.radians) + tan(declination.radians) * sin(EclipticCoordinates.obliquityOfTheEcliptic.radians) - - //Step8: - let xEquatorialToEcliptic = cos(rightAscension.radians) - - //Step9: - var r: Angle = .init(radians: atan(yEquatorialToEcliptic / xEquatorialToEcliptic)) - - //Step9: - switch (yEquatorialToEcliptic >= 0,xEquatorialToEcliptic >= 0){ - - case (true, true): - break - case (true,false): - r.degrees += 180 - case(false,true): - r.degrees += 360 - case(false,false): - r.degrees += 180 + /// To set right ascension we need LST and hour angle. If hour angle is nill we can't set it. + /// - Parameter lstDecimal: Local Sideral Time in decimal + /// - Returns: The value of right ascension just been set. Nil if hour angle is also nil + public mutating func setRightAscensionFrom(lstDecimal: Double) -> Angle? { + guard let hourAngle = self.hourAngle else { + return nil } - - let eclipticLongitude: Angle = .init(degrees: r.degrees) - return .init(eclipticLatitude: eclipticLatitude, eclipticLongitude: eclipticLongitude) + let hourAngleDecimal = hourAngle.degrees / 15 + self.rightAscension = .init(degrees: lstDecimal - hourAngleDecimal) + + return self.rightAscension } } diff --git a/Sources/SunKit/HorizonCoordinates.swift b/Sources/SunKit/Sun Coordinates/HorizonCoordinates.swift similarity index 95% rename from Sources/SunKit/HorizonCoordinates.swift rename to Sources/SunKit/Sun Coordinates/HorizonCoordinates.swift index 74cc82f..9baf52a 100644 --- a/Sources/SunKit/HorizonCoordinates.swift +++ b/Sources/SunKit/Sun Coordinates/HorizonCoordinates.swift @@ -1,6 +1,6 @@ // // HoorizonCoordinates.swift -// +// // // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano // @@ -18,27 +18,24 @@ import Foundation + public struct HorizonCoordinates: Equatable, Hashable, Codable, Sendable { - public var altitude: Angle public var azimuth: Angle - + /// Converts horizon coordinates to equatorial coordinates /// - Returns: Equatorial coordinates of the instance. - public func horizon2Equatorial(latitude: Angle) -> EquatorialCoordinates{ - + public func horizon2Equatorial(latitude: Angle) -> EquatorialCoordinates { let tZeroHorizonToEquatorial = sin(altitude.radians) * sin(latitude.radians) + cos(altitude.radians) * cos(latitude.radians) * cos(azimuth.radians) let declination: Angle = .init(radians:asin(tZeroHorizonToEquatorial)) - let tOneHorizonToEquatorial = sin(altitude.radians) - sin(latitude.radians) * sin(declination.radians) - let tTwoHorizonToEquatorial = tOneHorizonToEquatorial / (cos(latitude.radians) * cos(declination.radians)) - var hourAngle: Angle = .init(radians: acos(tTwoHorizonToEquatorial)) - if sin(altitude.radians) >= 0{ + if sin(altitude.radians) >= 0 { hourAngle.degrees = 360 - hourAngle.degrees } + return .init(declination: declination, hourAngle: hourAngle) } } diff --git a/Sources/SunKit/Sun Coordinates/SunCoordinates.swift b/Sources/SunKit/Sun Coordinates/SunCoordinates.swift new file mode 100644 index 0000000..dc5be5f --- /dev/null +++ b/Sources/SunKit/Sun Coordinates/SunCoordinates.swift @@ -0,0 +1,194 @@ +// +// SunCoordinates.swift +// +// +// Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + + +internal struct SunCoordinates: Sendable { + internal var eclipticCoordinates: EclipticCoordinates + internal var equatorialCoordinates: EquatorialCoordinates + internal var horizonCoordinates: HorizonCoordinates + + internal let sunEclipticLongitudeAtTheEpoch: Angle = .init(degrees: 280.466069) + internal let sunEclipticLongitudePerigee: Angle = .init(degrees: 282.938346) + + internal var azimuth: Angle { + horizonCoordinates.azimuth + } + + internal var altitude: Angle { + horizonCoordinates.altitude + } + + init( + eclipticCoordinates: EclipticCoordinates, + equatorialCoordinates: EquatorialCoordinates, + horizonCoordinates: HorizonCoordinates + ) { + self.eclipticCoordinates = eclipticCoordinates + self.equatorialCoordinates = equatorialCoordinates + self.horizonCoordinates = horizonCoordinates + } + + init() { + self.eclipticCoordinates = EclipticCoordinates(eclipticLatitude: .zero, eclipticLongitude: .zero) + self.equatorialCoordinates = EquatorialCoordinates(declination: .zero) + self.horizonCoordinates = HorizonCoordinates(altitude: .zero, azimuth: .zero) + } + + // MARK: - Coordinates + + internal mutating func setSunCoordinates(using date: Date, longitude: Angle, latitude: Angle) { + let lstDecimal = calculateLSTDecimal(using: date, longitude: longitude) + let sunEclipticLongitude: Angle = calculateSunEclipticLongitude(using: date) + + setCoordinates( + sunEclipticLongitude: sunEclipticLongitude, + lstDecimal: lstDecimal, + latitude: latitude + ) + } + + private mutating func setCoordinates( + sunEclipticLongitude: Angle, + lstDecimal: Double, + latitude: Angle + ) { + setSunEclipticCoordinates(using: sunEclipticLongitude) + setSunEquatorialCoordinates(using: eclipticCoordinates) + setSunHorizonCoordinates(using: lstDecimal, latitude: latitude) + } + + internal func getSunHorizonCoordinates( + given date: Date, + longitude: Angle, + latitude: Angle + ) -> HorizonCoordinates { + let lstDecimal = calculateLSTDecimal(using: date, longitude: longitude) + let sunEclipticLongitude: Angle = calculateSunEclipticLongitude(using: date) + let sunHorizonCoordinates = getSunHorizonCoordinates( + given: sunEclipticLongitude, + lstDecimal: lstDecimal, + latitude: latitude + ) + + return .init(altitude: sunHorizonCoordinates.altitude, azimuth: sunHorizonCoordinates.azimuth) + } + + private func getSunHorizonCoordinates( + given sunEclipticLongitude: Angle, + lstDecimal: Double, + latitude: Angle + ) -> HorizonCoordinates { + let eclipticCoordinates = calculateSunEclipticCoordinates(using: sunEclipticLongitude) + let equatorialCoordinates = calculateSunEquatorialCoordinates(using: eclipticCoordinates) + let horizonCoordinates = calculateSunHorizonCoordinates( + using: equatorialCoordinates, + lstDecimal: lstDecimal, + latitude: latitude + ) + + return horizonCoordinates + } + + // MARK: Ecliptic Coordinates + + private mutating func setSunEclipticCoordinates(using sunEclipticLongitude: Angle) { + self.eclipticCoordinates = calculateSunEclipticCoordinates(using: sunEclipticLongitude) + } + + internal func calculateSunEclipticCoordinates(using sunEclipticLongitude: Angle) -> EclipticCoordinates { + EclipticCoordinates(eclipticLatitude: .zero, eclipticLongitude: sunEclipticLongitude) + } + + // MARK: Equatorial Coordinates + + private mutating func setSunEquatorialCoordinates(using sunEclipticCoordinates: EclipticCoordinates) { + self.equatorialCoordinates = calculateSunEquatorialCoordinates(using: sunEclipticCoordinates) + } + + internal func calculateSunEquatorialCoordinates(using sunEclipticCoordinates: EclipticCoordinates) -> EquatorialCoordinates { + sunEclipticCoordinates.ecliptic2Equatorial() + } + + // MARK: Horizon Coordinates + + private mutating func setSunHorizonCoordinates( + using lstDecimal: Double, + latitude: Angle + ) { + self.horizonCoordinates = calculateSunHorizonCoordinates( + using: equatorialCoordinates, + lstDecimal: lstDecimal, + latitude: latitude + ) + } + + internal func calculateSunHorizonCoordinates( + using sunEquatorialCoordinates: EquatorialCoordinates, + lstDecimal: Double, + latitude: Angle + ) -> HorizonCoordinates { + var _sunEquatorialCoordinates = sunEquatorialCoordinates + let horizonCoordinates = _sunEquatorialCoordinates.equatorial2Horizon(lstDecimal: lstDecimal, latitude: latitude) + + return horizonCoordinates ?? .init(altitude: .zero, azimuth: .zero) + } + + // MARK: - Helpers + + internal func calculateLSTDecimal(using date: Date, longitude: Angle) -> Double { + // Convert LCT to UT, GST, and LST times and adjust the date if needed + let gstHMS = uT2GST(date) + let lstHMS = gST2LST(gstHMS, longitude: longitude) + let lstDecimal = lstHMS.hMS2Decimal() + + return lstDecimal + } + + internal func calculateSunEclipticLongitude(using date: Date) -> Angle { + // Julian number for standard epoch 2000 + let jdEpoch = 2451545.00 + // Compute the Julian day number for the desired date using the Greenwich date and TT + let jdTT = jdFromDate(date: date) + // Compute the total number of elapsed days, including fractional days, since the standard epoch (i.e., JD − JDe) + let elapsedDaysSinceStandardEpoch: Double = jdTT - jdEpoch + // 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. + let sunMeanAnomaly = getSunMeanAnomaly(from: elapsedDaysSinceStandardEpoch) + // Use Equation 6.2.4 to aproximate the Equation of the center + let equationOfCenter = 360 / Double.pi * sin(sunMeanAnomaly.radians) * 0.016708 + // Add EoC to sun mean anomaly to get the sun true anomaly + var sunTrueAnomaly = sunMeanAnomaly.degrees + equationOfCenter + // Add or subtract multiples of 360° to adjust sun true anomaly to the range of 0° to 360° + sunTrueAnomaly = extendedMod(sunTrueAnomaly, 360) + var sunEclipticLongitude: Angle = .init(degrees: sunTrueAnomaly + sunEclipticLongitudePerigee.degrees) + + if sunEclipticLongitude.degrees > 360 { + sunEclipticLongitude.degrees -= 360 + } + + return sunEclipticLongitude + } + + private func getSunMeanAnomaly(from elapsedDaysSinceStandardEpoch: Double) -> Angle { + var sunMeanAnomaly: Angle = .init(degrees: (((360.0 * elapsedDaysSinceStandardEpoch) / 365.242191) + sunEclipticLongitudeAtTheEpoch.degrees - sunEclipticLongitudePerigee.degrees)) + sunMeanAnomaly = .init(degrees: extendedMod(sunMeanAnomaly.degrees, 360)) + + return sunMeanAnomaly + } +} diff --git a/Sources/SunKit/Sun/Extensions/Sun+Equatable.swift b/Sources/SunKit/Sun/Extensions/Sun+Equatable.swift new file mode 100644 index 0000000..271f41f --- /dev/null +++ b/Sources/SunKit/Sun/Extensions/Sun+Equatable.swift @@ -0,0 +1,28 @@ +// +// Sun+Equatable.swift +// +// +// Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + + +extension Sun: Equatable { + public static func == (lhs: Sun, rhs: Sun) -> Bool { + lhs.location == rhs.location && + lhs.timeZone == rhs.timeZone && + lhs.date == rhs.date + } +} diff --git a/Sources/SunKit/Sun/Extensions/Sun+Hashable.swift b/Sources/SunKit/Sun/Extensions/Sun+Hashable.swift new file mode 100644 index 0000000..bbcbce5 --- /dev/null +++ b/Sources/SunKit/Sun/Extensions/Sun+Hashable.swift @@ -0,0 +1,28 @@ +// +// Sun+Hashable.swift +// +// +// Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + + +extension Sun: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(location) + hasher.combine(timeZone) + hasher.combine(date) + } +} diff --git a/Sources/SunKit/Sun/Extensions/Sun+debugging.swift b/Sources/SunKit/Sun/Extensions/Sun+debugging.swift new file mode 100644 index 0000000..dffa9f7 --- /dev/null +++ b/Sources/SunKit/Sun/Extensions/Sun+debugging.swift @@ -0,0 +1,60 @@ +// +// Sun+debugging.swift +// +// +// Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + + +extension Sun { + /// Dumps all the Sun Events dates. + public func dumpDateInfos() { + print("Current Date -> \(dateFormatter.string(from: date))") + print("Sunrise -> \(dateFormatter.string(from: sunrise))") + print("Sunset -> \(dateFormatter.string(from: sunset))") + print("Solar Noon -> \(dateFormatter.string(from: solarNoon))") + print("Solar Midnight -> \(dateFormatter.string(from: solarMidnight))") + print("Evening Golden Hour Start -> \(dateFormatter.string(from: eveningGoldenHourStart))") + print("Evening Golden Hour End -> \(dateFormatter.string(from: eveningGoldenHourEnd))") + print("Morning Golden Hour Start -> \(dateFormatter.string(from: morningGoldenHourStart))") + print("Morning Golden Hour End -> \(dateFormatter.string(from: morningGoldenHourEnd))") + print("Civil dusk -> \(dateFormatter.string(from: civilDusk))") + print("Civil Dawn -> \(dateFormatter.string(from: civilDawn))") + print("Nautical Dusk -> \(dateFormatter.string(from: nauticalDusk))") + print("Nautical Dawn -> \(dateFormatter.string(from: nauticalDawn))") + print("Astronomical Dusk -> \(dateFormatter.string(from: astronomicalDusk))") + print("Astronomical Dawn -> \(dateFormatter.string(from: astronomicalDawn))") + print("Morning Blue Hour Start -> \(dateFormatter.string(from: morningBlueHourStart))") + print("Morning Blue Hour End -> \(dateFormatter.string(from: morningBlueHourEnd))") + print("evening Blue Hour Start -> \(dateFormatter.string(from: eveningBlueHourStart))") + print("evening Blue Hour End -> \(dateFormatter.string(from: eveningBlueHourEnd))") + + print("March Equinox -> \(dateFormatter.string(from: marchEquinox))") + print("June Solstice -> \(dateFormatter.string(from: juneSolstice))") + print("September Equinox -> \(dateFormatter.string(from: septemberEquinox))") + print("December Solstice -> \(dateFormatter.string(from: decemberSolstice))") + } + + private var dateFormatter: DateFormatter { + let dateFormatter = DateFormatter() + dateFormatter.locale = .current + dateFormatter.timeZone = self.timeZone + dateFormatter.timeStyle = .full + dateFormatter.dateStyle = .full + + return dateFormatter + } +} diff --git a/Sources/SunKit/Sun.swift b/Sources/SunKit/Sun/Sun.swift similarity index 53% rename from Sources/SunKit/Sun.swift rename to Sources/SunKit/Sun/Sun.swift index 4b7e79f..9b3eb0a 100644 --- a/Sources/SunKit/Sun.swift +++ b/Sources/SunKit/Sun/Sun.swift @@ -19,6 +19,7 @@ import Foundation import CoreLocation + public struct Sun: Identifiable, Sendable { public let id: UUID = UUID() @@ -34,97 +35,84 @@ public struct Sun: Identifiable, Sendable { Sun Events during the day *-------------------------------------------------------------------*/ - ///Date of Sunrise + /* + Sun Events during the day organized chronologically + */ + public private(set) var astronomicalDawn: Date = Date() + public private(set) var nauticalDawn: Date = Date() + public private(set) var civilDawn: Date = Date() + public private(set) var morningGoldenHourStart: Date = Date() public private(set) var sunrise: Date = Date() - ///Date of Sunset - public private(set) var sunset: Date = Date() - ///Date of Solar Noon for + public private(set) var morningGoldenHourEnd: Date = Date() + public private(set) var solarNoon: Date = Date() - ///Date of Solar Midnight - public private(set) var solarMidnight: Date = Date() - ///Date at which evening evening Golden hour starts public private(set) var eveningGoldenHourStart: Date = Date() - ///Date at which evening evening Golden hour ends + public private(set) var sunset: Date = Date() public private(set) var eveningGoldenHourEnd: Date = Date() - - ///Date at which evening Morning Golden hour starts - public private(set) var morningGoldenHourStart: Date = Date() - ///Date at which evening Morning Golden hour ends - public private(set) var morningGoldenHourEnd: Date = Date() - - - ///Date at which there is the Civil Dusk public private(set) var civilDusk: Date = Date() - ///Date at which there is the Civil Dawn - public private(set) var civilDawn: Date = Date() - - ///Date at which there is the Nautical Dusk public private(set) var nauticalDusk: Date = Date() - ///Date at which there is the Nautical Dawn - public private(set) var nauticalDawn: Date = Date() - - ///Date at which there is the Astronomical Dusk public private(set) var astronomicalDusk: Date = Date() - ///Date at which there is the Astronomical Dawn - public private(set) var astronomicalDawn: Date = Date() + public private(set) var solarMidnight: Date = Date() - ///Date at which morning Blue Hour starts. Sun at -6 degrees elevation = civil dusk - public var morningBlueHourStart: Date{ + /// Date at which the morning Blue Hour starts. + /// The same elevation as Civil Dawn--when the Sun is at -6 degrees. + public var morningBlueHourStart: Date { civilDawn } - ///Date at which morning Blue Hour ends. Sun at -4 degrees elevation = morning golden hour start + /// Date at which morning Blue Hour ends and the morning Golden Hour begins. + /// The same elevation as morning Golden Hour start--when the Sun is at -4 degrees. public var morningBlueHourEnd: Date { morningGoldenHourStart } - ///Date at which evening Blue Hour starts. Sun at -4 degrees elevation = evening golden hour end - public var eveningBlueHourStart: Date{ + /// Date at which evening Golden Hour ends and the evening Blue Hour begins. + /// The same elevation as morning Golden Hour end--when the Sun is at -4 degrees. + public var eveningBlueHourStart: Date { eveningGoldenHourEnd } - ///Date at which morning Blue Hour ends. Sun at -6 degrees elevation = Civil Dawn + /// Date at which evening Blue Hour ends. + /// The same elevation as Civil Dusk--when the Sun is at -6 degrees. public var eveningBlueHourEnd: Date { civilDusk } - /*-------------------------------------------------------------------- Sun Azimuths for Self.date and for Sunrise,Sunset and Solar Noon *-------------------------------------------------------------------*/ - ///Azimuth of Sunrise public private(set) var sunriseAzimuth: Double = 0 - ///Azimuth of Sunset - public private(set) var sunsetAzimuth: Double = 0 - ///Azimuth of Solar noon public private(set) var solarNoonAzimuth: Double = 0 + public private(set) var sunsetAzimuth: Double = 0 + + /// The object backing the public interface for Sun coordinates. + private var sunCoordinates: SunCoordinates = .init() - // Sun azimuth for (Location,Date) in Self public var azimuth: Angle { - sunHorizonCoordinates.azimuth + sunCoordinates.azimuth } - // Sun altitude for (Location,Date) in Self public var altitude: Angle { - sunHorizonCoordinates.altitude + sunCoordinates.altitude + } + + public var sunEquatorialCoordinates: EquatorialCoordinates { + sunCoordinates.equatorialCoordinates } - public private(set) var sunEquatorialCoordinates: EquatorialCoordinates = .init(declination: .zero) - public private(set) var sunEclipticCoordinates: EclipticCoordinates = .init(eclipticLatitude: .zero, eclipticLongitude: .zero) + public var sunEclipticCoordinates: EclipticCoordinates { + sunCoordinates.eclipticCoordinates + } /*-------------------------------------------------------------------- Sun Events during the year *-------------------------------------------------------------------*/ - ///Date at which there will be march equinox public private(set) var marchEquinox: Date = Date() - ///Date at which there will be june solstice public private(set) var juneSolstice: Date = Date() - ///Date at which there will be september solstice public private(set) var septemberEquinox: Date = Date() - ///Date at which there will be december solstice public private(set) var decemberSolstice: Date = Date() /*-------------------------------------------------------------------- @@ -141,21 +129,21 @@ public struct Sun: Identifiable, Sendable { .init(degrees: location.coordinate.latitude) } - /// Returns daylight time in seconds + /// Returns daylight time in seconds. public var totalDayLightTime: Int { let diffComponents = calendar.dateComponents([.second], from: sunrise, to: sunset) return diffComponents.second ?? 0 } - /// Returns night time in seconds + /// Returns night time in seconds. public var totalNightTime: Int { - let startOfTheDay = calendar.startOfDay(for: date) - let endOfTheDay = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: startOfTheDay)! - var diffComponents = calendar.dateComponents([.second], from: startOfTheDay, to: sunrise) + let startOfTheDay = calendar.startOfDay(for: date) + let endOfTheDay = calendar.date(bySettingHour: 23, minute: 59, second: 59, of: startOfTheDay)! + var diffComponents = calendar.dateComponents([.second], from: startOfTheDay, to: sunrise) var nightHours: Int = diffComponents.second ?? 0 - diffComponents = calendar.dateComponents([.second], from: sunset, to: endOfTheDay) - nightHours = nightHours + (diffComponents.second ?? 0) + diffComponents = calendar.dateComponents([.second], from: sunset, to: endOfTheDay) + nightHours = nightHours + (diffComponents.second ?? 0) return nightHours } @@ -204,7 +192,6 @@ public struct Sun: Identifiable, Sendable { isMorningBlueHour || isEveningBlueHour } - /// 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 public var isCircumPolar: Bool { isAlwaysDay || isAlwaysNight @@ -255,18 +242,16 @@ public struct Sun: Identifiable, Sendable { public mutating func setDate(_ newDate: Date) { let newDay = calendar.dateComponents([.day,.month,.year], from: newDate) let oldDay = calendar.dateComponents([.day,.month,.year], from: date) - let isSameDay: Bool = (newDay == oldDay) date = newDate - refresh(needToComputeSunEvents: !isSameDay) //If is the same day no need to compute again Daily Sun Events + refresh(needToComputeSunEvents: !isSameDay) // If is the same day no need to compute again Daily Sun Events } /*-------------------------------------------------------------------- Changing Location *-------------------------------------------------------------------*/ - /// Changing location and timezone /// - Parameters: /// - newLocation: New location @@ -284,7 +269,6 @@ public struct Sun: Identifiable, Sendable { refresh() } - /// Is highly recommanded to use the other method to change both location and timezone. This will be kept only for backwards retrocompatibility. /// - Parameters: /// - newLocation: New Location @@ -296,7 +280,6 @@ public struct Sun: Identifiable, Sendable { refresh() } - /*-------------------------------------------------------------------- Changing Timezone *-------------------------------------------------------------------*/ @@ -316,83 +299,30 @@ public struct Sun: Identifiable, Sendable { refresh() } - /*-------------------------------------------------------------------- - Debug functions - *-------------------------------------------------------------------*/ - - /// Dumps all the Sun Events dates - public func dumpDateInfos(){ - - print("Current Date -> \(dateFormatter.string(from: date))") - print("Sunrise -> \(dateFormatter.string(from: sunrise))") - print("Sunset -> \(dateFormatter.string(from: sunset))") - print("Solar Noon -> \(dateFormatter.string(from: solarNoon))") - print("Solar Midnight -> \(dateFormatter.string(from: solarMidnight))") - print("Evening Golden Hour Start -> \(dateFormatter.string(from: eveningGoldenHourStart))") - print("Evening Golden Hour End -> \(dateFormatter.string(from: eveningGoldenHourEnd))") - print("Morning Golden Hour Start -> \(dateFormatter.string(from: morningGoldenHourStart))") - print("Morning Golden Hour End -> \(dateFormatter.string(from: morningGoldenHourEnd))") - print("Civil dusk -> \(dateFormatter.string(from: civilDusk))") - print("Civil Dawn -> \(dateFormatter.string(from: civilDawn))") - print("Nautical Dusk -> \(dateFormatter.string(from: nauticalDusk))") - print("Nautical Dawn -> \(dateFormatter.string(from: nauticalDawn))") - print("Astronomical Dusk -> \(dateFormatter.string(from: astronomicalDusk))") - print("Astronomical Dawn -> \(dateFormatter.string(from: astronomicalDawn))") - print("Morning Blue Hour Start -> \(dateFormatter.string(from: morningBlueHourStart))") - print("Morning Blue Hour End -> \(dateFormatter.string(from: morningBlueHourEnd))") - print("evening Blue Hour Start -> \(dateFormatter.string(from: eveningBlueHourStart))") - print("evening Blue Hour End -> \(dateFormatter.string(from: eveningBlueHourEnd))") - - print("March Equinox -> \(dateFormatter.string(from: marchEquinox))") - print("June Solstice -> \(dateFormatter.string(from: juneSolstice))") - print("September Equinox -> \(dateFormatter.string(from: septemberEquinox))") - print("December Solstice -> \(dateFormatter.string(from: decemberSolstice))") - } - /*-------------------------------------------------------------------- Private Variables *-------------------------------------------------------------------*/ private var calendar: Calendar { var calendar: Calendar = .init(identifier: .gregorian) - calendar.timeZone = self.timeZone + calendar.timeZone = self.timeZone return calendar } - private var dateFormatter: DateFormatter { - let dateFormatter = DateFormatter() - dateFormatter.locale = .current - dateFormatter.timeZone = self.timeZone - dateFormatter.timeStyle = .full - dateFormatter.dateStyle = .full - return dateFormatter - } - private var timeZoneInSeconds: Int { timeZone.secondsFromGMT(for: self.date) } - private var sunHorizonCoordinates: HorizonCoordinates = .init(altitude: .zero, azimuth: .zero) - - - //Sun constants - private let sunEclipticLongitudeAtTheEpoch: Angle = .init(degrees: 280.466069) - private let sunEclipticLongitudePerigee: Angle = .init(degrees: 282.938346) - /// Number of the days passed since the start of the year for the self.date private var daysPassedFromStartOfTheYear: Int { - let year = calendar.component(.year, from: date) - + let year = calendar.component(.year, from: date) let dateFormatter: DateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy/mm/dd" dateFormatter.calendar = calendar let dataFormatted = dateFormatter.date(from: "\(year)/01/01") - let startOfYear = calendar.startOfDay(for: dataFormatted!) let startOfDay = calendar.startOfDay(for: date) - var daysPassedFromStartOfTheYear = calendar.dateComponents([.day], from: startOfYear, to: startOfDay).day! daysPassedFromStartOfTheYear = daysPassedFromStartOfTheYear + 1 @@ -412,22 +342,35 @@ public struct Sun: Identifiable, Sendable { } private var localStandardTimeMeridian: Double { - return (Double(self.timeZoneInSeconds) / SECONDS_IN_ONE_HOUR) * 15 //TimeZone in hour + return (Double(self.timeZoneInSeconds) / SECONDS_IN_ONE_HOUR) * 15 // TimeZone in hour } private var timeCorrectionFactorInSeconds: Double { - let timeCorrectionFactor = 4 * (location.coordinate.longitude - localStandardTimeMeridian) + equationOfTime - let minutes: Double = Double(Int(timeCorrectionFactor) * 60) - let seconds: Double = timeCorrectionFactor.truncatingRemainder(dividingBy: 1) * 100 - let timeCorrectionFactorInSeconds = minutes + seconds + let timeCorrectionFactor = 4 * (location.coordinate.longitude - localStandardTimeMeridian) + equationOfTime + let minutes: Double = Double(Int(timeCorrectionFactor) * 60) + let seconds: Double = timeCorrectionFactor.truncatingRemainder(dividingBy: 1) * 100 + let timeCorrectionFactorInSeconds = minutes + seconds return timeCorrectionFactorInSeconds } - - /*-------------------------------------------------------------------- - Private methods - *-------------------------------------------------------------------*/ + /// - Returns: Length in meters of the object's shadow by the provided object height and current sun altitude. + public func shadowLength( + for objectHeight: Double = 1, + with altitude: Angle? = nil + ) -> Double? { + let altitude = altitude ?? self.altitude + + return if altitude.degrees > 0 && altitude.degrees < 90 { + objectHeight / tan(altitude.radians) + } else if altitude.degrees <= 0 { + nil + } else { + 0 + } + } + + // MARK: - Refresh Sun State /// Updates in order all the sun coordinates: horizon, ecliptic and equatorial. /// Then get rise, set and noon times and their relative azimuths in degrees. @@ -437,267 +380,179 @@ public struct Sun: Identifiable, Sendable { /// /// - Parameter needToComputeAgainSunEvents: True if Sunrise,Sunset and all the others daily sun events have to be computed. private mutating func refresh(needToComputeSunEvents: Bool = true) { - updateSunCoordinates() - - if(needToComputeSunEvents){ - self.sunrise = getSunrise() ?? Date() - self.sunriseAzimuth = getSunHorizonCoordinatesFrom(date: sunrise).azimuth.degrees - self.sunset = getSunset() ?? Date() - self.sunsetAzimuth = getSunHorizonCoordinatesFrom(date: sunset).azimuth.degrees - self.solarNoon = getSolarNoon() ?? Date() - self.solarMidnight = getSolarMidnight() ?? Date() - self.solarNoonAzimuth = getSunHorizonCoordinatesFrom(date: solarNoon).azimuth.degrees - self.eveningGoldenHourStart = getEveningGoldenHourStart() ?? Date() - self.eveningGoldenHourEnd = getEveningGoldenHourEnd() ?? Date() - self.civilDusk = getCivilDusk() ?? Date() - self.civilDawn = getCivilDawn() ?? Date() - self.nauticalDusk = getNauticalDusk() ?? Date() - self.nauticalDawn = getNauticalDawn() ?? Date() - self.astronomicalDusk = getAstronomicalDusk() ?? Date() - self.astronomicalDawn = getAstronomicalDawn() ?? Date() + updateSunCoordinates(using: self.date, longitude: self.longitude, latitude: self.latitude) + + if (needToComputeSunEvents) { + self.astronomicalDawn = getAstronomicalDawn() ?? Date() + self.nauticalDawn = getNauticalDawn() ?? Date() + self.civilDawn = getCivilDawn() ?? Date() self.morningGoldenHourStart = getMorningGoldenHourStart() ?? Date() - self.morningGoldenHourEnd = getMorningGoldenHourEnd() ?? Date() + self.sunrise = getSunrise() ?? Date() + self.morningGoldenHourEnd = getMorningGoldenHourEnd() ?? Date() + self.solarNoon = getSolarNoon() ?? Date() + + self.eveningGoldenHourStart = getEveningGoldenHourStart() ?? Date() + self.sunset = getSunset() ?? Date() + self.eveningGoldenHourEnd = getEveningGoldenHourEnd() ?? Date() + self.civilDusk = getCivilDusk() ?? Date() + self.nauticalDusk = getNauticalDusk() ?? Date() + self.astronomicalDusk = getAstronomicalDusk() ?? Date() + self.solarMidnight = getSolarMidnight() ?? Date() + + self.sunriseAzimuth = getSunHorizonCoordinatesFrom(date: sunrise).azimuth.degrees + self.solarNoonAzimuth = getSunHorizonCoordinatesFrom(date: solarNoon).azimuth.degrees + self.sunsetAzimuth = getSunHorizonCoordinatesFrom(date: sunset).azimuth.degrees } - self.marchEquinox = getMarchEquinox() ?? Date() - self.juneSolstice = getJuneSolstice() ?? Date() + self.marchEquinox = getMarchEquinox() ?? Date() + self.juneSolstice = getJuneSolstice() ?? Date() self.septemberEquinox = getSeptemberEquinox() ?? Date() self.decemberSolstice = getDecemberSolstice() ?? Date() } - private func getSunMeanAnomaly(from elapsedDaysSinceStandardEpoch: Double) -> Angle { - //Compute mean anomaly sun - var sunMeanAnomaly: Angle = .init(degrees:(((360.0 * elapsedDaysSinceStandardEpoch) / 365.242191) + sunEclipticLongitudeAtTheEpoch.degrees - sunEclipticLongitudePerigee.degrees)) - sunMeanAnomaly = .init(degrees: extendedMod(sunMeanAnomaly.degrees, 360)) - - return sunMeanAnomaly + /// Updates Horizon coordinates, Ecliptic coordinates and Equatorial coordinates of the Sun + private mutating func updateSunCoordinates(using date: Date, longitude: Angle, latitude: Angle) { + sunCoordinates.setSunCoordinates(using: date, longitude: longitude, latitude: latitude) } - private func getSunEclipticLongitude(from sunMeanAnomaly: Angle) -> Angle { - //eclipticLatitude - let equationOfCenter = 360 / Double.pi * sin(sunMeanAnomaly.radians) * 0.016708 - let trueAnomaly = sunMeanAnomaly.degrees + equationOfCenter - var eclipticLatitude: Angle = .init(degrees: trueAnomaly + sunEclipticLongitudePerigee.degrees) - - if eclipticLatitude.degrees > 360 { - eclipticLatitude.degrees -= 360 - } + public func getSunHorizonCoordinatesFrom(date: Date) -> HorizonCoordinates { + let sunHorizonCoordinates = sunCoordinates.getSunHorizonCoordinates( + given: date, + longitude: self.longitude, + latitude: self.latitude + ) - return eclipticLatitude + return sunHorizonCoordinates } + // MARK: - Get Day Events - /// Updates Horizon coordinates, Ecliptic coordinates and Equatorial coordinates of the Sun - private mutating func updateSunCoordinates() { - //Step1: - //Convert LCT to UT, GST, and LST times and adjust the date if needed - let gstHMS = uT2GST(self.date) - let lstHMS = gST2LST(gstHMS,longitude: longitude) - - let lstDecimal = lstHMS.hMS2Decimal() - - //Step2: - //Julian number for standard epoch 2000 - let jdEpoch = 2451545.00 - - //Step3: - //Compute the Julian day number for the desired date using the Greenwich date and TT - - let jdTT = jdFromDate(date: self.date) - - //Step5: - //Compute the total number of elapsed days, including fractional days, since the standard epoch (i.e., JD − JDe) - let elapsedDaysSinceStandardEpoch: Double = jdTT - jdEpoch //De - - //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. - let sunMeanAnomaly = getSunMeanAnomaly(from: elapsedDaysSinceStandardEpoch) - - //Step7: Use Equation 6.2.4 to aproximate the Equation of the center - let equationOfCenter = 360 / Double.pi * sin(sunMeanAnomaly.radians) * 0.016708 - - //Step8: Add EoC to sun mean anomaly to get the sun true anomaly - var sunTrueAnomaly = sunMeanAnomaly.degrees + equationOfCenter - - //Step9: - sunTrueAnomaly = extendedMod(sunTrueAnomaly, 360) - - //Step10: - var sunEclipticLongitude: Angle = .init(degrees: sunTrueAnomaly + sunEclipticLongitudePerigee.degrees) - - //Step11: - if sunEclipticLongitude.degrees > 360 { - sunEclipticLongitude.degrees -= 360 + /// Astronomical Dawn is when the Sun reaches -18 degrees of elevation. + private func getAstronomicalDawn() -> Date? { + guard let astronomicalDawn = getDateFrom(sunEvent: .astronomical, morning: true) else { + return nil } - sunEclipticCoordinates = .init(eclipticLatitude: .zero, eclipticLongitude: sunEclipticLongitude) - - //Step12: Ecliptic to Equatorial - sunEquatorialCoordinates = sunEclipticCoordinates.ecliptic2Equatorial() - - //Step13: Equatorial to Horizon - sunHorizonCoordinates = sunEquatorialCoordinates.equatorial2Horizon(lstDecimal: lstDecimal,latitude: latitude) ?? .init(altitude: .zero, azimuth: .zero) - + return astronomicalDawn } - public func getSunHorizonCoordinatesFrom(date: Date) -> HorizonCoordinates { - //Step1: - //Convert LCT to UT, GST, and LST times and adjust the date if needed - let gstHMS = uT2GST(date) - let lstHMS = gST2LST(gstHMS,longitude: longitude) - - let lstDecimal = lstHMS.hMS2Decimal() - - //Step2: - //Julian number for standard epoch 2000 - let jdEpoch = 2451545.00 - - //Step3: - //Compute the Julian day number for the desired date using the Greenwich date and TT - - let jdTT = jdFromDate(date: date) - - //Step5: - //Compute the total number of elapsed days, including fractional days, since the standard epoch (i.e., JD − JDe) - let elapsedDaysSinceStandardEpoch: Double = jdTT - jdEpoch //De - - //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. - let sunMeanAnomaly = getSunMeanAnomaly(from: elapsedDaysSinceStandardEpoch) - - //Step7: Use Equation 6.2.4 to aproximate the Equation of the center - let equationOfCenter = 360 / Double.pi * sin(sunMeanAnomaly.radians) * 0.016708 - - //Step8: Add EoC to sun mean anomaly to get the sun true anomaly - var sunTrueAnomaly = sunMeanAnomaly.degrees + equationOfCenter - - //Step9: Add or subtract multiples of 360° to adjust sun true anomaly to the range of 0° to 360° - sunTrueAnomaly = extendedMod(sunTrueAnomaly, 360) - - - //Step10: Getting ecliptic longitude. - var sunEclipticLongitude: Angle = .init(degrees: sunTrueAnomaly + sunEclipticLongitudePerigee.degrees) - - //Step11: - if sunEclipticLongitude.degrees > 360 { - sunEclipticLongitude.degrees -= 360 + /// Nautical Dusk is when the Sun reaches -12 degrees of elevation. + private func getNauticalDawn() -> Date? { + guard let nauticalDawn = getDateFrom(sunEvent: .nautical, morning: true) else { + return nil } - let sunEclipticCoordinates: EclipticCoordinates = .init(eclipticLatitude: .zero, eclipticLongitude: sunEclipticLongitude) - - //Step12: Ecliptic to Equatorial - var sunEquatorialCoordinates: EquatorialCoordinates = sunEclipticCoordinates.ecliptic2Equatorial() - - //Step13: Equatorial to Horizon - let sunHorizonCoordinates: HorizonCoordinates = sunEquatorialCoordinates.equatorial2Horizon(lstDecimal: lstDecimal,latitude: latitude) ?? .init(altitude: .zero, azimuth: .zero) - - return .init(altitude: sunHorizonCoordinates.altitude, azimuth: sunHorizonCoordinates.azimuth) + return nauticalDawn } - /// Computes the solar noon for self.date. Solar noon is the time when the sun is highest in the sky. - /// - Returns: Solar noon time - private func getSolarNoon() -> Date? { - let secondsForUTCSolarNoon = (720 - 4 * location.coordinate.longitude - equationOfTime) * 60 - let secondsForSolarNoon = secondsForUTCSolarNoon + Double(timeZoneInSeconds) - let startOfTheDay = calendar.startOfDay(for: date) - - let solarNoon = calendar.date(byAdding: .second, value: Int(secondsForSolarNoon) , to: startOfTheDay) + /// Civil Dawn is when the Sun reaches -6 degrees of elevation. + private func getCivilDawn() -> Date? { + guard let civilDawn = getDateFrom(sunEvent: .civil,morning: true) else { + return nil + } - return solarNoon + return civilDawn } - /// Computes the solar midnight for self.date. - /// - Returns: Solar midnight time - private func getSolarMidnight() -> Date? { - - let secondsForUTCSolarMidnight = (0 - 4 * location.coordinate.longitude - equationOfTime) * 60 - let secondsForSolarMidnight = secondsForUTCSolarMidnight + Double(timeZoneInSeconds) - let startOfTheDay = calendar.startOfDay(for: date) + /// Morning Golden Hour starts when the Sun reaches -4 degrees of elevation. + private func getMorningGoldenHourStart() -> Date? { + guard let morningGoldenHourStart = getDateFrom(sunEvent: .morningGoldenHourStart, morning: true) else { + return nil + } - let solarMidnight = calendar.date(byAdding: .second, value: Int(secondsForSolarMidnight) , to: startOfTheDay) - - return solarMidnight + return morningGoldenHourStart } - - - /// Computes the Sunrise time for self.date - /// - Returns: Sunrise time + /// Sunrise is when the Sun reaches 0 degrees of elevation, aka the horizon, at the start of the day. private func getSunrise() -> Date? { - var haArg = (cos(Angle.degrees(90.833).radians)) / (cos(latitude.radians) * cos(sunEquatorialCoordinates.declination.radians)) - tan(latitude.radians) * tan(sunEquatorialCoordinates.declination.radians) + let sunriseSeconds = calculateSunriseSeconds() + let sunriseDate = calculateDate(using: sunriseSeconds) - haArg = clamp(lower: -1, upper: 1, number: haArg) - let ha: Angle = .radians(acos(haArg)) - let sunriseUTCMinutes = 720 - 4 * (location.coordinate.longitude + ha.degrees) - equationOfTime - var sunriseSeconds = (Int(sunriseUTCMinutes) * 60) + timeZoneInSeconds - let startOfDay = calendar.startOfDay(for: date) + return sunriseDate + } + + private func calculateSunriseSeconds() -> Int { + let solarHourAngle = calculateSunriseSolarHourAngle() + let sunriseUTCMinutes = calculateUTCMinutes(using: solarHourAngle) + var sunriseSeconds = calculateSeconds(using: sunriseUTCMinutes) if sunriseSeconds < Int(SECONDS_IN_ONE_HOUR) { sunriseSeconds = 0 } - let hoursMinutesSeconds: (Int, Int, Int) = secondsToHoursMinutesSeconds(Int(sunriseSeconds)) - - let sunriseDate = calendar.date(bySettingHour: hoursMinutesSeconds.0, minute: hoursMinutesSeconds.1, second: hoursMinutesSeconds.2, of: startOfDay) - - return sunriseDate + return sunriseSeconds } - /// Computes the Sunset time for self.date - /// - Returns: Sunset time - private func getSunset() -> Date? { - var haArg = (cos(Angle.degrees(90.833).radians)) / (cos(latitude.radians) * cos(sunEquatorialCoordinates.declination.radians)) - tan(latitude.radians) * tan(sunEquatorialCoordinates.declination.radians) - - haArg = clamp(lower: -1, upper: 1, number: haArg) - let ha: Angle = .radians(-acos(haArg)) - let sunsetUTCMinutes = 720 - 4 * (location.coordinate.longitude + ha.degrees) - equationOfTime - var sunsetSeconds = (Int(sunsetUTCMinutes) * 60) + timeZoneInSeconds - let startOfDay = calendar.startOfDay(for: date) + private func calculateSunriseSolarHourAngle() -> Angle { + let cosSolarHourAngle = clampCosineOfSolarHourAngle() + let solarHourAngle: Angle = .radians(acos(cosSolarHourAngle)) - if sunsetSeconds > SECONDS_IN_ONE_DAY { - sunsetSeconds = SECONDS_IN_ONE_DAY - } + return solarHourAngle + } + + private func clampCosineOfSolarHourAngle() -> Double { + let cosSolarHourAngle = calculateCosineOfSolarHourAngle() - let hoursMinutesSeconds: (Int, Int, Int) = secondsToHoursMinutesSeconds(Int(sunsetSeconds)) + return clamp(lower: -1, upper: 1, number: cosSolarHourAngle) + } + + private func calculateCosineOfSolarHourAngle() -> Double { + let solarZenithAngle = 90.833 + let cosSolarZenithAngle = cos(Angle.degrees(solarZenithAngle).radians) + let cosLatitude = cos(latitude.radians) + let cosSunDeclination = cos(sunEquatorialCoordinates.declination.radians) + let tanLatitude = tan(latitude.radians) + let tanSunDeclination = tan(sunEquatorialCoordinates.declination.radians) - let sunsetDate = calendar.date(bySettingHour: hoursMinutesSeconds.0, minute: hoursMinutesSeconds.1, second: hoursMinutesSeconds.2, of: startOfDay) + return cosSolarZenithAngle / (cosLatitude * cosSunDeclination) - tanLatitude * tanSunDeclination + } + + private func calculateUTCMinutes(using solarHourAngle: Angle) -> Double { + let longitude = location.coordinate.longitude - return sunsetDate + return 720 - 4 * (longitude + solarHourAngle.degrees) - equationOfTime } - /// Computes the time at which the sun will reach the elevation given in input for self.date - /// - Parameters: - /// - elevation: Elevation - /// - 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. - /// - Returns: Time at which the Sun reaches that elevation. Nil if it didn't find it. - private func getDateFrom(sunEvent : SunElevationEvents, morning: Bool = false) -> Date? { + private func calculateSeconds(using UTCMinutes: Double) -> Int { + let UTCSeconds = Int(UTCMinutes) * 60 - let elevationSun: Angle = .degrees(sunEvent.rawValue) - var cosHra = (sin(elevationSun.radians) - sin(sunEquatorialCoordinates.declination.radians) * sin(latitude.radians)) / (cos(sunEquatorialCoordinates.declination.radians) * cos(latitude.radians)) - cosHra = clamp(lower: -1, upper: 1, number: cosHra) - let hraAngle: Angle = .radians(acos(cosHra)) - var secondsForSunToReachElevation = (morning ? -1 : 1) * (hraAngle.degrees / 15) * SECONDS_IN_ONE_HOUR + TWELVE_HOUR_IN_SECONDS - timeCorrectionFactorInSeconds - let startOfTheDay = calendar.startOfDay(for: date) + return UTCSeconds + timeZoneInSeconds + } + + private func calculateDate(using seconds: Int) -> Date? { + let hoursMinutesSeconds: (Int, Int, Int) = secondsToHoursMinutesSeconds(Int(seconds)) + let startOfDay = calendar.startOfDay(for: date) + let date = calendar.date( + bySettingHour: hoursMinutesSeconds.0, + minute: hoursMinutesSeconds.1, + second: hoursMinutesSeconds.2, + of: startOfDay + ) - if (Int(secondsForSunToReachElevation) > SECONDS_IN_ONE_DAY){ - - secondsForSunToReachElevation = Double(SECONDS_IN_ONE_DAY) - } - else if (secondsForSunToReachElevation < SECONDS_IN_ONE_HOUR){ - - secondsForSunToReachElevation = 0 + return date + } + + /// Morning Golden Hour ends when Sun reaches 6 degrees of elevation. + private func getMorningGoldenHourEnd() -> Date? { + guard let morningGoldenHourEnd = getDateFrom(sunEvent: .morningGoldenHourEnd, morning: true) else { + return nil } - let hoursMinutesSeconds: (Int, Int, Int) = secondsToHoursMinutesSeconds(Int(secondsForSunToReachElevation)) - let newDate = calendar.date(bySettingHour: hoursMinutesSeconds.0 , minute: hoursMinutesSeconds.1, second: hoursMinutesSeconds.2, of: startOfTheDay) - - - return newDate + return morningGoldenHourEnd } + /// Solar Noon is the time when the Sun is highest in the sky. + private func getSolarNoon() -> Date? { + let secondsForUTCSolarNoon = (720 - 4 * location.coordinate.longitude - equationOfTime) * 60 + let secondsForSolarNoon = secondsForUTCSolarNoon + Double(timeZoneInSeconds) + let startOfTheDay = calendar.startOfDay(for: date) + let solarNoon = calendar.date(byAdding: .second, value: Int(secondsForSolarNoon), to: startOfTheDay) + + return solarNoon + } - - /// Golden Hour in the evening begins when the sun reaches elevation equals to 6 degrees - /// - Returns: Time at which the GoldenHour starts + /// Evening Golden Hour begins when the Sun reaches 6 degrees of elevation. private func getEveningGoldenHourStart() -> Date? { guard let eveningGoldenHourStart = getDateFrom(sunEvent: .eveningGoldenHourStart) else { return nil @@ -706,159 +561,162 @@ public struct Sun: Identifiable, Sendable { return eveningGoldenHourStart } - /// Golden Hour in the evening ends when the sun reaches elevation equals to -4 degrees - /// - Returns: Time at which the GoldenHour ends - private func getEveningGoldenHourEnd() -> Date? { - guard let goldenHourFinish = getDateFrom(sunEvent: .eveningGoldenHourEnd) else { - return nil + /// Sunset is when the Sun reaches 0 degrees of elevation, aka the horizon, at the end of the day. + private func getSunset() -> Date? { + let sunsetSeconds = calculateSunsetSeconds() + let sunsetDate = calculateDate(using: sunsetSeconds) + + return sunsetDate + } + + private func calculateSunsetSeconds() -> Int { + let solarHourAngle = calculateSunsetSolarHourAngle() + let sunsetUTCMinutes = calculateUTCMinutes(using: solarHourAngle) + var sunsetSeconds = calculateSeconds(using: sunsetUTCMinutes) + + if sunsetSeconds > SECONDS_IN_ONE_DAY { + sunsetSeconds = SECONDS_IN_ONE_DAY } - return goldenHourFinish + return sunsetSeconds } - /// Civil Dawn is when the Sun reaches -6 degrees of elevation. Also known as civil sunrise - /// - Returns: Civil Dawn time - private func getCivilDawn() -> Date? { - guard let civilDawn = getDateFrom(sunEvent: .civil,morning: true) else { + private func calculateSunsetSolarHourAngle() -> Angle { + let cosSolarHourAngle = clampCosineOfSolarHourAngle() + let solarHourAngle: Angle = .radians(-acos(cosSolarHourAngle)) + + return solarHourAngle + } + + /// Evening Golden Hour ends when the sun reaches -4 degrees of elevation. + private func getEveningGoldenHourEnd() -> Date? { + guard let goldenHourFinish = getDateFrom(sunEvent: .eveningGoldenHourEnd) else { return nil } - return civilDawn + + return goldenHourFinish } - /// civil dusk is when the Sun reaches -6 degrees of elevation. Also known as civil sunrise. - /// - Returns: civil dusk time + /// Civil Dusk is when the Sun reaches -6 degrees of elevation. private func getCivilDusk() -> Date? { guard let civilDusk = getDateFrom(sunEvent: .civil, morning: false) else { return nil } + return civilDusk } /// Nautical Dusk is when the Sun reaches -12 degrees of elevation. - /// - Returns: Nautical Dusk private func getNauticalDusk() -> Date? { guard let nauticalDusk = getDateFrom(sunEvent: .nautical, morning: false) else { return nil } + return nauticalDusk } - /// Nautical Dusk is when the Sun reaches -12 degrees of elevation. - /// - Returns: Nautical Dawn - private func getNauticalDawn() -> Date? { - guard let nauticalDawn = getDateFrom(sunEvent: .nautical, morning: true) else { - return nil - } - return nauticalDawn - } - /// Astronomical Dusk is when the Sun reaches -18 degrees of elevation. - /// - Returns: Astronomical Dusk private func getAstronomicalDusk() -> Date? { guard let astronomicalDusk = getDateFrom(sunEvent: .astronomical, morning: false) else { return nil } + return astronomicalDusk } - /// Astronomical Dawn is when the Sun reaches -18 degrees of elevation. - /// - Returns: Astronomical Dawn - private func getAstronomicalDawn() -> Date? { - guard let astronomicalDawn = getDateFrom(sunEvent: .astronomical, morning: true) else { - return nil - } - return astronomicalDawn + /// Computes the solar midnight for self.date. + private func getSolarMidnight() -> Date? { + let secondsForUTCSolarMidnight = (0 - 4 * location.coordinate.longitude - equationOfTime) * 60 + let secondsForSolarMidnight = secondsForUTCSolarMidnight + Double(timeZoneInSeconds) + let startOfTheDay = calendar.startOfDay(for: date) + let solarMidnight = calendar.date(byAdding: .second, value: Int(secondsForSolarMidnight), to: startOfTheDay) + + return solarMidnight } - /// Morning Golden Hour start when Sun reaches -4 degress of elevation - /// - Returns: Morning golden hour start - private func getMorningGoldenHourStart() -> Date? { - guard let morningGoldenHourStart = getDateFrom(sunEvent: .morningGoldenHourStart , morning: true) else { - return nil + /// Computes the time at which the sun will reach the elevation given in input for self.date + /// - Parameters: + /// - elevation: Elevation + /// - 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. + /// - Returns: Time at which the Sun reaches that elevation. Nil if it didn't find it. + private func getDateFrom( + sunEvent : SunElevationEvents, + morning: Bool = false + ) -> Date? { + let elevationSun: Angle = .degrees(sunEvent.rawValue) + var cosHra = (sin(elevationSun.radians) - sin(sunEquatorialCoordinates.declination.radians) * sin(latitude.radians)) / (cos(sunEquatorialCoordinates.declination.radians) * cos(latitude.radians)) + cosHra = clamp(lower: -1, upper: 1, number: cosHra) + let hraAngle: Angle = .radians(acos(cosHra)) + var secondsForSunToReachElevation = (morning ? -1 : 1) * (hraAngle.degrees / 15) * SECONDS_IN_ONE_HOUR + TWELVE_HOUR_IN_SECONDS - timeCorrectionFactorInSeconds + let startOfTheDay = calendar.startOfDay(for: date) + + if (Int(secondsForSunToReachElevation) > SECONDS_IN_ONE_DAY) { + secondsForSunToReachElevation = Double(SECONDS_IN_ONE_DAY) + } else if (secondsForSunToReachElevation < SECONDS_IN_ONE_HOUR) { + secondsForSunToReachElevation = 0 } - return morningGoldenHourStart + + let hoursMinutesSeconds: (Int, Int, Int) = secondsToHoursMinutesSeconds(Int(secondsForSunToReachElevation)) + let newDate = calendar.date(bySettingHour: hoursMinutesSeconds.0 , minute: hoursMinutesSeconds.1, second: hoursMinutesSeconds.2, of: startOfTheDay) + + return newDate } - /// Morning Golden Hour ends when Sun reaches 6 degress of elevation - /// - Returns: Morning golden hour end - private func getMorningGoldenHourEnd() -> Date? { - guard let morningGoldenHourEnd = getDateFrom(sunEvent: .morningGoldenHourEnd , morning: true) else { - return nil - } - return morningGoldenHourEnd - } + // MARK: - Get Month Events private func getMarchEquinox() -> Date? { - let year = Double(calendar.component(.year, from: self.date)) let t: Double = year / 1000 - let julianDayMarchEquinox: Double = 1721139.2855 + 365.2421376 * year + 0.0679190 * pow(t, 2) - 0.0027879 * pow(t, 3) + let marchSolsticeJulianDayConstant = 1721139.2855 + let marchSolsticeMeanYearLengthInDays = 365.2421376 + let quadraticCorrection = 0.0679190 + let cubicCorrection = 0.0027879 + let julianDayMarchEquinox: Double = marchSolsticeJulianDayConstant + marchSolsticeMeanYearLengthInDays * year + quadraticCorrection * pow(t, 2) - cubicCorrection * pow(t, 3) let marchEquinoxUTC = dateFromJd(jd: julianDayMarchEquinox) return marchEquinoxUTC } private func getJuneSolstice() -> Date? { - let year = Double(calendar.component(.year, from: self.date)) let t: Double = year / 1000 - let julianDayJuneSolstice: Double = 1721233.2486 + 365.2417284 * year - 0.0530180 * pow(t, 2) + 0.0093320 * pow(t, 3) + let juneSolsticeJulianDayConstant = 1721233.2486 + let juneSolsticeMeanYearLengthInDays = 365.2417284 + let quadraticCorrection = 0.0530180 + let cubicCorrection = 0.0093320 + let julianDayJuneSolstice: Double = juneSolsticeJulianDayConstant + juneSolsticeMeanYearLengthInDays * year - quadraticCorrection * pow(t, 2) + cubicCorrection * pow(t, 3) let juneSolsticeUTC = dateFromJd(jd: julianDayJuneSolstice) return juneSolsticeUTC } private func getSeptemberEquinox() -> Date? { - let year = Double(calendar.component(.year, from: self.date)) let t: Double = year / 1000 - let julianDaySeptemberEquinox: Double = 1721325.6978 + 365.2425055 * year - 0.126689 * pow(t, 2) + 0.0019401 * pow(t, 3) + let septemberSolsticeJulianDayConstant = 1721325.6978 + let septemberSolsticeMeanYearLengthInDays = 365.2425055 + let quadraticCorrection = 0.126689 + let cubicCorrection = 0.0019401 + let julianDaySeptemberEquinox: Double = septemberSolsticeJulianDayConstant + septemberSolsticeMeanYearLengthInDays * year - quadraticCorrection * pow(t, 2) + cubicCorrection * pow(t, 3) let septemberEquinoxUTC = dateFromJd(jd: julianDaySeptemberEquinox) return septemberEquinoxUTC } private func getDecemberSolstice() -> Date? { - let year = Double(calendar.component(.year, from: self.date)) let t: Double = year / 1000 - let julianDayDecemberSolstice: Double = 1721414.3920 + 365.2428898 * year - 0.0109650 * pow(t, 2) - 0.0084885 * pow(t, 3) + let decemberSolsticeJulianDayConstant = 1721414.3920 + let decemberSolsticeMeanYearLengthInDays = 365.2428898 + let quadraticCorrection = 0.0109650 + let cubicCorrection = 0.0084885 + let julianDayDecemberSolstice: Double = decemberSolsticeJulianDayConstant + decemberSolsticeMeanYearLengthInDays * year - quadraticCorrection * pow(t, 2) - cubicCorrection * pow(t, 3) let decemberSolsticeUTC = dateFromJd(jd: julianDayDecemberSolstice) return decemberSolsticeUTC } - - - /// - Returns: Length in meters of the object's shadow by the provided object height and current sun altitude. - public func shadowLength(for objectHeight: Double = 1, with altitude: Angle? = nil) -> Double? { - let altitude = altitude ?? self.altitude - - return if altitude.degrees > 0 && altitude.degrees < 90 { - objectHeight / tan(altitude.radians) - } else if altitude.degrees <= 0 { - nil - } else { - 0 - } - } -} - -extension Sun: Equatable { - public static func == (lhs: Sun, rhs: Sun) -> Bool { - lhs.location == rhs.location && - lhs.timeZone == rhs.timeZone && - lhs.date == rhs.date - } -} - -extension Sun: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(location) - hasher.combine(timeZone) - hasher.combine(date) - } } diff --git a/Sources/SunKit/SunElevationEvents.swift b/Sources/SunKit/SunElevationEvents.swift index fcaf6d3..dc002fb 100644 --- a/Sources/SunKit/SunElevationEvents.swift +++ b/Sources/SunKit/SunElevationEvents.swift @@ -18,8 +18,8 @@ import Foundation -enum SunElevationEvents: Double{ - + +enum SunElevationEvents: Double { case civil = -6 case nautical = -12 case astronomical = -18 @@ -28,5 +28,4 @@ enum SunElevationEvents: Double{ static var morningGoldenHourStart: SunElevationEvents { .eveningGoldenHourEnd } static var morningGoldenHourEnd: SunElevationEvents { .eveningGoldenHourStart } - } diff --git a/Sources/SunKit/Utils.swift b/Sources/SunKit/Utils.swift index de30e34..c7f4f49 100644 --- a/Sources/SunKit/Utils.swift +++ b/Sources/SunKit/Utils.swift @@ -1,6 +1,6 @@ // // Utils.swift -// +// // // Copyright 2024 Leonardo Bertinelli, Davide Biancardi, Raffaele Fulgente, Clelia Iovine, Nicolas Mariniello, Fabio Pizzano // @@ -18,6 +18,7 @@ import Foundation + let SECONDS_IN_ONE_DAY = 86399 let TWELVE_HOUR_IN_SECONDS: Double = 43200 let TWO_HOURS_IN_SECONDS: Double = 7200 @@ -29,38 +30,32 @@ let SECONDS_IN_ONE_HOUR: Double = 3600 /// - a: first operand /// - n: second operand /// - 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. - public func mod(_ a: Int, _ n: Int) -> Int { - let r = a % n - return r >= 0 ? r : r + n - } - - - /// 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. - /// - Parameters: - /// - a: first operand - /// - n: second operand - /// - 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. - public func extendedMod(_ a: Double, _ n: Int) -> Double { - - let remainder: Double = a.truncatingRemainder(dividingBy: 1) - - if (a < 0 && a > -1){ - - return Double(n) + remainder - } - - let x = Double(mod(Int(a),n)) - - return x + remainder - } - - +public func mod(_ a: Int, _ n: Int) -> Int { + let r = a % n + + return r >= 0 ? r : r + n +} -public func clamp(lower: Double, upper: Double, number: Double) -> Double { +/// 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. +/// - Parameters: +/// - a: first operand +/// - n: second operand +/// - 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. +public func extendedMod(_ a: Double, _ n: Int) -> Double { + let remainder: Double = a.truncatingRemainder(dividingBy: 1) - return min(upper,max(lower,number)) - } + if (a < 0 && a > -1) { + return Double(n) + remainder + } + + let x = Double(mod(Int(a), n)) + + return x + remainder +} +public func clamp(lower: Double, upper: Double, number: Double) -> Double { + return min(upper, max(lower, number)) +} /// Creates a date with the UTC timezone /// - Parameters: @@ -72,8 +67,15 @@ public func clamp(lower: Double, upper: Double, number: Double) -> Double { /// - seconds: second of the date you want to create /// - nanosecond: nanosecond of the date you want to create /// - Returns: A date with the parameters given in input. Used in combination with function jdFromDate, that accepts dates in UTC format -public func createDateUTC(day: Int,month: Int,year: Int,hour: Int,minute: Int,seconds: Int, nanosecond: Int = 0) -> Date{ - +public func createDateUTC( + day: Int, + month: Int, + year: Int, + hour: Int, + minute: Int, + seconds: Int, + nanosecond: Int = 0 +) -> Date { var calendarUTC:Calendar = .init(identifier: .gregorian) calendarUTC.timeZone = .init(secondsFromGMT: 0)! var dateComponents = DateComponents() @@ -88,7 +90,6 @@ public func createDateUTC(day: Int,month: Int,year: Int,hour: Int,minute: Int,se return calendarUTC.date(from: dateComponents) ?? Date() } - /// Creates a date with the timezone used in your current device /// - Parameters: /// - day: day of the date you want to create @@ -99,8 +100,15 @@ public func createDateUTC(day: Int,month: Int,year: Int,hour: Int,minute: Int,se /// - seconds: second of the date you want to create /// - nanosecond: nanosecond of the date you want to create /// - Returns: A date with the parameters given in input -public func createDateCurrentTimeZone(day: Int,month: Int,year: Int,hour: Int,minute: Int,seconds: Int, nanosecond: Int = 0) -> Date{ - +public func createDateCurrentTimeZone( + day: Int, + month: Int, + year: Int, + hour: Int, + minute: Int, + seconds: Int, + nanosecond: Int = 0 +) -> Date { var calendar: Calendar = .init(identifier: .gregorian) calendar.timeZone = .current var dateComponents = DateComponents() @@ -126,8 +134,16 @@ public func createDateCurrentTimeZone(day: Int,month: Int,year: Int,hour: Int,mi /// - nanosecond: nanosecond of the date you want to create /// - timeZone: timezone of the date /// - Returns: A date with the parameters given in input -public func createDateCustomTimeZone(day: Int,month: Int,year: Int,hour: Int,minute: Int,seconds: Int, nanosecond: Int = 0, timeZone: TimeZone) -> Date{ - +public func createDateCustomTimeZone( + day: Int, + month: Int, + year: Int, + hour: Int, + minute: Int, + seconds: Int, + nanosecond: Int = 0, + timeZone: TimeZone +) -> Date { var calendar: Calendar = .init(identifier: .gregorian) calendar.timeZone = timeZone var dateComponents = DateComponents() @@ -142,7 +158,6 @@ public func createDateCustomTimeZone(day: Int,month: Int,year: Int,hour: Int,min return calendar.date(from: dateComponents) ?? Date() } - /// Converts Julian Number in a date /// - Parameter jd: Julian number to convert in an UTC date /// - Returns: The date corresponding to the julian number in input @@ -151,61 +166,55 @@ public func dateFromJd(jd : Double) -> Date { return Date(timeIntervalSince1970: (jd - JD_JAN_1_1970_0000GMT) * 86400) } - /// Converts date in his Julian Number /// - Parameter date: UTC date to convert in julian number. TimeZone of the given date shall be equals to +0000. /// - Returns: The julian day number corresponding to date in input public func jdFromDate(date : Date) -> Double { let JD_JAN_1_1970_0000GMT = 2440587.5 - return JD_JAN_1_1970_0000GMT + date.timeIntervalSince1970 - / 86400 + + return JD_JAN_1_1970_0000GMT + date.timeIntervalSince1970 / 86400 } - /// Converts UT time to Greenwich Sidereal Time /// - Parameter ut: UT time to convert in GST /// - Parameter timeZoneInSeconds: time zone expressed in seconds of your local civil time /// - Returns: GST equivalent of the UT given in input -public func uT2GST(_ ut:Date) -> HMS{ - +public func uT2GST(_ ut:Date) -> HMS { var calendarUTC: Calendar = .init(identifier: .gregorian) calendarUTC.timeZone = TimeZone(identifier: "GMT")! - //Step1: + // Step1: let jd = jdFromDate(date: calendarUTC.startOfDay(for: ut)) - //Step2: + // Step2: let year = calendarUTC.component(.year, from: ut) let firstDayOfTheYear = calendarUTC.date(from: DateComponents(year: year , month: 1, day: 1)) ?? Date() let jdZero = jdFromDate(date: firstDayOfTheYear) - //Step3: + // Step3: let days = jd - jdZero - //Step4: + // Step4: let T = (jdZero - 2415020.0) / 36525.0 - //Step5: + // Step5: let R = 6.6460656 + 2400.051262 * T + 0.00002581*(T*T) - //Step6: + // Step6: let B :Double = 24 - R + Double(24 * (year - 1900)) - //Step7: + // Step7: let TZero = 0.0657098 * days - B - //Step8: + // Step8: let utDecimal = HMS.init(from: ut).hMS2Decimal() - //Step9: + // Step9: var gstDecimal = TZero + 1.002738 * utDecimal if gstDecimal < 0 { - gstDecimal += 24 - - }else if gstDecimal >= 24 { - + } else if gstDecimal >= 24 { gstDecimal -= 24 } @@ -220,8 +229,7 @@ public func uT2GST(_ ut:Date) -> HMS{ /// - longitude: longitude of the observer /// - Parameter timeZoneInSeconds: time zone expressed in seconds of your local civil time /// - Returns: LST equivalent for the GST given in input -public func gST2LST(_ gst: HMS, longitude: Angle) -> HMS{ - +public func gST2LST(_ gst: HMS, longitude: Angle) -> HMS { //Step1: let gstDecimal = gst.hMS2Decimal() @@ -230,25 +238,23 @@ public func gST2LST(_ gst: HMS, longitude: Angle) -> HMS{ //Step3: var lstDecimal = gstDecimal + adjustment - + if lstDecimal < 0 { lstDecimal += 24 - }else if lstDecimal >= 24 { + } else if lstDecimal >= 24 { lstDecimal -= 24 } - + let lstHMS = HMS.init(decimal: lstDecimal) return lstHMS } - /// Converts the number of seconds in HH:MM:ss /// - Parameter seconds: Number of seconds that have to be converted /// - Returns: From value in input the equivalent in (hours,minute,seconds) -public func secondsToHoursMinutesSeconds(_ seconds : Int) -> (Int,Int,Int) { - +public func secondsToHoursMinutesSeconds(_ seconds : Int) -> (Int, Int, Int) { return (seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60) } diff --git a/Tests/SunKitTests/UT_DMS.swift b/Tests/SunKitTests/UT_DMS.swift index d11f31a..602d0df 100644 --- a/Tests/SunKitTests/UT_DMS.swift +++ b/Tests/SunKitTests/UT_DMS.swift @@ -19,13 +19,12 @@ import XCTest @testable import SunKit + final class UT_DMS: XCTestCase { - /// Test of DMS init with decimal in input - func testOfDMSDecimailInit(){ - - //Test1: Convert −0.508333° to DMS format. The result shall be -0°30''30'. + func testOfDMSDecimailInit() { + //Test1: Convert −0.508333° to DMS format. The result shall be -0°30''30'. //Step1: Initialize dmsUnderTest to −0.508333° and saving the expected output var dmsUnderTest: DMS = .init(decimal: -0.508333) @@ -33,7 +32,7 @@ final class UT_DMS: XCTestCase { //Step2: Check if the the values set is inside the correct range and isANegativeZero is TRUE XCTAssertTrue((dmsUnderTest.degrees == 0) && (dmsUnderTest.minutes == 30) && (29.99...30).contains(dmsUnderTest.seconds) && dmsUnderTest.isANegativeZero) - //Test2: Convert 0.508333° to DMS format. The result shall be 0°30''30'. + //Test2: Convert 0.508333° to DMS format. The result shall be 0°30''30'. //Step3: Initialize dmsUnderTest to 0.508333° and saving the expected output dmsUnderTest = .init(decimal: 0.508333) @@ -41,32 +40,29 @@ final class UT_DMS: XCTestCase { //Step4: Check if the the values set is inside the correct range and isANegativeZero is FAlse XCTAssertTrue((dmsUnderTest.degrees == 0) && (dmsUnderTest.minutes == 30) && (29.99...30).contains(dmsUnderTest.seconds) && !dmsUnderTest.isANegativeZero) - //Test3: Convert -300.3333° to DMS format. The result shall be -300° 20′ 00′′. - + //Test3: Convert -300.3333° to DMS format. The result shall be -300° 20′ 00′′. + //Step5: Creating a DMS instance of and saving the expected output dmsUnderTest = .init(decimal: -300.3333) //Step6: Call function under test and check that it returns a value close to expected output XCTAssertTrue((dmsUnderTest.degrees == -300) && (19...20).contains(dmsUnderTest.minutes) && (59...59.99).contains(dmsUnderTest.seconds) && !dmsUnderTest.isANegativeZero) - } - /// Test of dMS2Decimal - func testOfdMS2Decimal(){ - - //Test1: For 300° 20′ 00′′ his decimal number should be 300.3333 + func testOfdMS2Decimal() { + //Test1: For 300° 20′ 00′′ his decimal number should be 300.3333 //Step1: Creating a DMS instance of 300° 20′ 00′′ and saving the expected output var dmsUnderTest: DMS = .init(degrees: 300, minutes: 20, seconds: 00) var expectedOutput: Double = 300.3333 //Step2: Call function under test and check that it returns a value close to expected output XCTAssertTrue(abs(dmsUnderTest.dMS2Decimal() - expectedOutput) < 0.01) - - //Test2: For -0°30''30' his decimal number should be -0.508333 + + //Test2: For -0°30''30' his decimal number should be -0.508333 //Step3: Creating a DMS instance of -0°30''30' and saving the expected output - dmsUnderTest = .init(degrees: 0, minutes: 30, seconds: 30,isANegativeZero: true) + dmsUnderTest = .init(degrees: 0, minutes: 30, seconds: 30, isANegativeZero: true) expectedOutput = -0.508333 //Step4: Call function under test and check that it returns a value close to expected output diff --git a/Tests/SunKitTests/UT_EclipticCoordinates.swift b/Tests/SunKitTests/UT_EclipticCoordinates.swift index c694f22..88dab5f 100644 --- a/Tests/SunKitTests/UT_EclipticCoordinates.swift +++ b/Tests/SunKitTests/UT_EclipticCoordinates.swift @@ -20,12 +20,12 @@ import XCTest @testable import SunKit import SwiftUI -final class UT_EclipticCoordinates: XCTestCase { +final class UT_EclipticCoordinates: XCTestCase { + /// Test of ecliptic2Equatorial func testOfecliptic2Equatorial() throws { - - //Test1: Convert ecliptic coordinates with latitude = −3.956258 and longitude = 65.059853. Expected Equatorial coordinates are declination = 17.248880 and right ascension = 4.257714. + //Test1: Convert ecliptic coordinates with latitude = −3.956258 and longitude = 65.059853. Expected Equatorial coordinates are declination = 17.248880 and right ascension = 4.257714. //Step1: let eclipticCoordinatesUnderTest: EclipticCoordinates = .init(eclipticLatitude: .init(degrees: -3.956258), eclipticLongitude: .init(degrees: 65.059853)) @@ -40,9 +40,5 @@ final class UT_EclipticCoordinates: XCTestCase { //Step3: Check if output of the funciton under test is close to expected output for both right ascension and declination XCTAssertTrue(abs(rightAscension - expectedRightAscension) < 0.1) XCTAssertTrue(abs(declination - expectedDeclination) < 0.1) - - - } - } diff --git a/Tests/SunKitTests/UT_EquatorialCoordinates.swift b/Tests/SunKitTests/UT_EquatorialCoordinates.swift index e4496a7..32832cf 100644 --- a/Tests/SunKitTests/UT_EquatorialCoordinates.swift +++ b/Tests/SunKitTests/UT_EquatorialCoordinates.swift @@ -19,13 +19,12 @@ import XCTest @testable import SunKit -final class UT_EquatorialCoordinates: XCTestCase { +final class UT_EquatorialCoordinates: XCTestCase { /// Test of EquatorialCoordinates init func testOfInitEquatorialCoordinates() throws { - - //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. + //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. //Step1: let declinationUnderTest: Angle = .init(degrees: DMS.init(degrees:0 , minutes: 30, seconds: 30,isANegativeZero: true).dMS2Decimal()) @@ -39,12 +38,9 @@ final class UT_EquatorialCoordinates: XCTestCase { } - /// Test of equatorial2Horizon func testOfequatorial2Horizon() throws { - - - //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 + //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 //Step1: var equatorialCoordinatesUnderTest: EquatorialCoordinates = .init(declination: .init(degrees: 17.248880), rightAscension: .init(degrees: 4.257714)) @@ -62,10 +58,9 @@ final class UT_EquatorialCoordinates: XCTestCase { XCTAssertTrue(abs(azimuth - expectedAzimuth) < 0.1) XCTAssertTrue(abs(altitude - expectedAltitude) < 0.1) + //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′′. - //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′′. - - //Step4: + //Step4: let hourAngleDecimal = HMS.init(hours: 16, minutes: 29, seconds: 45).hMS2Decimal() let hourAngle: Angle = .init(degrees: hourAngleDecimal * 15) latitudeUnderTest = .degrees(25) @@ -88,8 +83,7 @@ final class UT_EquatorialCoordinates: XCTestCase { /// Test of equatorial2Ecliptic func testOfequatorial2Ecliptic() throws { - - //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′′ + //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′′ //Step1: let rightAscensionUnderTest = HMS.init(hours: 12, minutes: 18, seconds: 47.5).hMS2Decimal() @@ -101,9 +95,9 @@ final class UT_EquatorialCoordinates: XCTestCase { //Step2: Saving expected values in output for both latitude and longitude let expectedLatitude = DMS.init(degrees: 1, minutes: 12, seconds: 0).dMS2Decimal() let expectedLongitude = DMS.init(degrees: 184, minutes: 36, seconds: 0).dMS2Decimal() - + //Step3: Check if output of the function under test is close to expected output for both latitude and longitude XCTAssertTrue(abs(expectedLatitude - eclipticCoordinates.eclipticLatitude.degrees) < 0.1) XCTAssertTrue(abs(expectedLongitude - eclipticCoordinates.eclipticLongitude.degrees) < 0.1) - } + } } diff --git a/Tests/SunKitTests/UT_HMS.swift b/Tests/SunKitTests/UT_HMS.swift index 8783cdb..20f9fc8 100644 --- a/Tests/SunKitTests/UT_HMS.swift +++ b/Tests/SunKitTests/UT_HMS.swift @@ -19,12 +19,12 @@ import XCTest @testable import SunKit + final class UT_HMS: XCTestCase { /// Test of hMS2Decimal /// For 10h 25m 11s his decimal number should be 10.419722 func testOfhMS2Decimal() throws { - //Step1: Creating a HMS instance of 10h 25m 11s let hmsUnderTest: HMS = .init(hours: 10, minutes: 25, seconds: 11) let expectedOutput: Double = 10.419722 @@ -32,12 +32,10 @@ final class UT_HMS: XCTestCase { //Step2: Call function under test and check that it returns a value close to expected output XCTAssertTrue(abs(hmsUnderTest.hMS2Decimal() - expectedOutput) < 0.01) } - + /// Test of HMSDecimalInit func testOfHMSDecimalInit() throws { - - - //Test1: For 10.419722 decimal his HMS should be 10h 25m 11s + //Test1: For 10.419722 decimal his HMS should be 10h 25m 11s //Step1: Creating a HMS instance of 10.419722 decimal number var hmsUnderTest: HMS = .init(decimal: 10.419722) @@ -45,7 +43,7 @@ final class UT_HMS: XCTestCase { //Step2: Call function under test and check that it returns a value close to expected output XCTAssertTrue((hmsUnderTest.hours == 10) && (hmsUnderTest.minutes == 25) && (10.9...11.1).contains(hmsUnderTest.seconds)) - //Test2: For decimal equals 20.352 the DMS value should be equals to 20h 21m 7.2s + //Test2: For decimal equals 20.352 the DMS value should be equals to 20h 21m 7.2s //Step3: Creating a HMS instance of 10.419722 decimal number hmsUnderTest = .init(decimal: 20.352) diff --git a/Tests/SunKitTests/UT_HorizonCoordinates.swift b/Tests/SunKitTests/UT_HorizonCoordinates.swift index 9db4a07..0833386 100644 --- a/Tests/SunKitTests/UT_HorizonCoordinates.swift +++ b/Tests/SunKitTests/UT_HorizonCoordinates.swift @@ -19,12 +19,12 @@ import XCTest @testable import SunKit + final class UT_HorizonCoordinates: XCTestCase { /// Test of horizon2Equatorial func testOfhorizon2Equatorial() throws { - - //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°. + //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°. //Step1: let horizonCoordinatesUnderTest: HorizonCoordinates = .init(altitude: .degrees(40), azimuth: .degrees(115)) diff --git a/Tests/SunKitTests/UT_Sun.swift b/Tests/SunKitTests/UT_Sun.swift index 4d5fb9b..69f2f32 100644 --- a/Tests/SunKitTests/UT_Sun.swift +++ b/Tests/SunKitTests/UT_Sun.swift @@ -21,8 +21,8 @@ import Foundation import CoreLocation @testable import SunKit + final class UT_Sun: XCTestCase { - /*-------------------------------------------------------------------- Thresholds. UTs will pass if |output - expectedOutput| < threshold *-------------------------------------------------------------------*/ @@ -31,7 +31,7 @@ final class UT_Sun: XCTestCase { static let sunSetRiseThresholdInSeconds: Double = 120 //2 minutes in seconds static let sunEquinoxesAndSolsticesThresholdInSeconds: Double = 700 // approxametly 11 minutes - static let objectShadowThreshold: Double = 0.01 + static let objectShadowThreshold: Double = 0.01 /*-------------------------------------------------------------------- Naples timezone and location @@ -66,7 +66,6 @@ final class UT_Sun: XCTestCase { static let mumbaiLocation: CLLocation = .init(latitude: 18.94017, longitude: 72.83489) static let timeZoneMumbai = 5.5 - /// Test of Sun azimuth, sunrise, sunset, evening golden hour start and evening golden hour end /// Value for expected results have been taken from SunCalc.org func testOfSun() throws { @@ -108,30 +107,30 @@ final class UT_Sun: XCTestCase { let expectedastronomicalDusk = createDateCustomTimeZone(day: 19, month: 11, year: 2022, hour: 18, minute: 17, seconds: 20,timeZone: timeZoneUnderTest) - + //Step4: Check if the output are close to the expected ones XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) - + XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectednauticalDusk.timeIntervalSince1970 - sunUnderTest.nauticalDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectednauticalDawn.timeIntervalSince1970 - sunUnderTest.nauticalDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedastronomicalDusk.timeIntervalSince1970 - sunUnderTest.astronomicalDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedastronomicalDawn.timeIntervalSince1970 - sunUnderTest.astronomicalDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + //Test: 31/12/2024 15:32. Timezone +1. Leap Year. @@ -158,21 +157,21 @@ final class UT_Sun: XCTestCase { expectedcivilDusk = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 17, minute: 16, seconds: 06,timeZone: timeZoneUnderTest) expectedSolarNoon = createDateCustomTimeZone(day: 31, month: 12, year: 2024, hour: 12, minute: 06, seconds: 11,timeZone: timeZoneUnderTest) - + //Step4: Check if the output are close to the expected ones XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) - + XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) @@ -204,23 +203,23 @@ final class UT_Sun: XCTestCase { expectedcivilDusk = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 19, minute: 14, seconds: 00,timeZone: timeZoneUnderTest) expectedSolarNoon = createDateCustomTimeZone(day: 1, month: 8, year: 2022, hour: 11, minute: 47, seconds: 36,timeZone: timeZoneUnderTest) - + //Step4: Check if the output are close to the expected ones XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) - + XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + /*-------------------------------------------------------------------- Louisa USA *-------------------------------------------------------------------*/ @@ -234,7 +233,7 @@ final class UT_Sun: XCTestCase { //Step2: Setting 1/01/15 22:00 as date. (No daylight saving) dateUnderTest = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 22, minute: 00, seconds: 00,timeZone: timeZoneUnderTest) sunUnderTest.setDate(dateUnderTest) - + //Step3: Saving expected outputs expectedAzimuth = 287.62 expectedAltitude = -57.41 @@ -249,21 +248,21 @@ final class UT_Sun: XCTestCase { expectedcivilDusk = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 17, minute: 32, seconds: 27,timeZone: timeZoneUnderTest) expectedSolarNoon = createDateCustomTimeZone(day: 1, month: 1, year: 2015, hour: 12, minute: 15, seconds: 23,timeZone: timeZoneUnderTest) - + //Step4: Check if the output are close to the expected ones XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) - + XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedGoldenHourStart.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourStart.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedGoldenHourEnd.timeIntervalSince1970 - sunUnderTest.eveningGoldenHourEnd.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedcivilDawn.timeIntervalSince1970 - sunUnderTest.civilDawn.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) - + XCTAssertTrue(abs(expectedSolarNoon.timeIntervalSince1970 - sunUnderTest.solarNoon.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) /*-------------------------------------------------------------------- @@ -290,12 +289,12 @@ final class UT_Sun: XCTestCase { expectedcivilDusk = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 15, minute: 05, seconds: 08,timeZone: timeZoneUnderTest) expectedSolarNoon = createDateCustomTimeZone(day: 19, month: 1, year: 2022, hour: 11, minute: 54, seconds: 52,timeZone: timeZoneUnderTest) - + //Step4: Check if the output are close to the expected ones XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) - + XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) @@ -327,12 +326,12 @@ final class UT_Sun: XCTestCase { expectedcivilDusk = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 19, minute: 09, seconds: 19,timeZone: timeZoneUnderTest) expectedSolarNoon = createDateCustomTimeZone(day: 12, month: 3, year: 2023, hour: 12, minute: 48, seconds: 31,timeZone: timeZoneUnderTest) - + //Step4: Check if the output are close to the expected ones XCTAssertTrue(abs(expectedAzimuth - sunUnderTest.azimuth.degrees) < UT_Sun.sunAzimuthThreshold) XCTAssertTrue(abs(expectedAltitude - sunUnderTest.altitude.degrees) < UT_Sun.sunAltitudeThreshold) - + XCTAssertTrue(abs(expectedSunRise.timeIntervalSince1970 - sunUnderTest.sunrise.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedSunset.timeIntervalSince1970 - sunUnderTest.sunset.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) XCTAssertTrue(abs(expectedcivilDusk.timeIntervalSince1970 - sunUnderTest.civilDusk.timeIntervalSince1970) < UT_Sun.sunSetRiseThresholdInSeconds) @@ -342,51 +341,42 @@ final class UT_Sun: XCTestCase { //Test for issue #26 in github guard let pst = TimeZone(abbreviation: "PST") else { - abort() - } - - guard let utc = TimeZone(abbreviation: "UTC") else { - abort() - } - - let location: CLLocation = .init(latitude: 34.052235, longitude: -118.243683) - - var sun = Sun.init(location: location, timeZone: pst) - - sun.setDate(SunKit.createDateCustomTimeZone(day: 11, month: 3, year: 2023, hour: 22, minute: 00, seconds: 00, timeZone: pst)) - XCTAssertEqual(sun.sunrise.toString(pst), "03/11, 06:08") - XCTAssertEqual(sun.sunset.toString(pst), "03/11, 17:58") - XCTAssertEqual(sun.sunrise.toString(utc), "03/11, 14:08") - XCTAssertEqual(sun.sunset.toString(utc), "03/12, 01:58") - - sun.setDate(SunKit.createDateCustomTimeZone(day: 13, month: 3, year: 2023, hour: 22, minute: 00, seconds: 00, timeZone: pst)) - XCTAssertEqual(sun.sunrise.toString(pst), "03/13, 07:06") - XCTAssertEqual(sun.sunset.toString(pst), "03/13, 18:59") - XCTAssertEqual(sun.sunrise.toString(utc), "03/13, 14:06") - XCTAssertEqual(sun.sunset.toString(utc), "03/14, 01:59") + abort() + } + + guard let utc = TimeZone(abbreviation: "UTC") else { + abort() + } + + let location: CLLocation = .init(latitude: 34.052235, longitude: -118.243683) + + var sun = Sun.init(location: location, timeZone: pst) + + sun.setDate(SunKit.createDateCustomTimeZone(day: 11, month: 3, year: 2023, hour: 22, minute: 00, seconds: 00, timeZone: pst)) + XCTAssertEqual(sun.sunrise.toString(pst), "03/11, 06:08") + XCTAssertEqual(sun.sunset.toString(pst), "03/11, 17:58") + XCTAssertEqual(sun.sunrise.toString(utc), "03/11, 14:08") + XCTAssertEqual(sun.sunset.toString(utc), "03/12, 01:58") + sun.setDate(SunKit.createDateCustomTimeZone(day: 13, month: 3, year: 2023, hour: 22, minute: 00, seconds: 00, timeZone: pst)) + XCTAssertEqual(sun.sunrise.toString(pst), "03/13, 07:06") + XCTAssertEqual(sun.sunset.toString(pst), "03/13, 18:59") + XCTAssertEqual(sun.sunrise.toString(utc), "03/13, 14:06") + XCTAssertEqual(sun.sunset.toString(utc), "03/14, 01:59") } - func testOfObjectShadow() throws{ - + func testOfObjectShadow() throws { //Step1: Creating sun instance in Naples and with timezone +1 (No daylight saving) let timeZoneUnderTest: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current -// let timeZoneDaylightSaving: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaplesDaylightSaving * Int(SECONDS_IN_ONE_HOUR)) ?? .current + // let timeZoneDaylightSaving: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaplesDaylightSaving * Int(SECONDS_IN_ONE_HOUR)) ?? .current let sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneUnderTest) - - XCTAssertTrue(abs(sunUnderTest.shadowLength(with: .init(degrees: 43.40))! - 1.06 ) <= UT_Sun.objectShadowThreshold) - XCTAssertTrue(sunUnderTest.shadowLength(with: .init(degrees: -0.01)) == nil) - XCTAssertTrue(sunUnderTest.shadowLength(with: .init(degrees: 90)) == 0) - - } func testOfEquinoxesAndSolstices() throws { - //Test: 19/01/22 17:31. Timezone +1. Naples //Step1: Creating sun instance in Naples and with timezone +1 (No daylight saving) @@ -399,31 +389,24 @@ final class UT_Sun: XCTestCase { sunUnderTest.setDate(dateUnderTest) //Step3: Saving expected outputs - let expectedMarchEquinox = createDateCustomTimeZone(day: 20, month: 3, year: 2022, hour: 16, minute: 33, seconds: 00,timeZone: timeZoneUnderTest) let expectedJuneSolstice = createDateCustomTimeZone(day: 21, month: 6, year: 2022, hour: 11, minute: 13, seconds: 00,timeZone: timeZoneDaylightSaving) let expectedSeptemberEquinox = createDateCustomTimeZone(day: 23, month: 09, year: 2022, hour: 03, minute: 03, seconds: 00,timeZone: timeZoneDaylightSaving) let expectedDecemberSolstice = createDateCustomTimeZone(day: 21, month: 12, year: 2022, hour: 22, minute: 47, seconds: 00,timeZone: timeZoneUnderTest) - + //Step4: Check if the output are close to the expected ones XCTAssertTrue(abs(expectedMarchEquinox.timeIntervalSince1970 - sunUnderTest.marchEquinox.timeIntervalSince1970) < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds) - XCTAssertTrue(abs(expectedJuneSolstice.timeIntervalSince1970 - sunUnderTest.juneSolstice.timeIntervalSince1970) < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds) - XCTAssertTrue(abs(expectedSeptemberEquinox.timeIntervalSince1970 - sunUnderTest.septemberEquinox.timeIntervalSince1970) < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds) - XCTAssertTrue(abs(expectedDecemberSolstice.timeIntervalSince1970 - sunUnderTest.decemberSolstice.timeIntervalSince1970) < UT_Sun.sunEquinoxesAndSolsticesThresholdInSeconds) - } - /// Test of a sun ionstance whenm you play with timezones and change location func testOfSunWhenTimezoneChanges() throws { - //Step1: Creating Sun instance in Naples and with timezone +1 let timeZoneNaples: TimeZone = .init(secondsFromGMT: UT_Sun.timeZoneNaples * Int(SECONDS_IN_ONE_HOUR)) ?? .current var sunUnderTest = Sun.init(location: UT_Sun.naplesLocation, timeZone: timeZoneNaples) @@ -441,11 +424,8 @@ final class UT_Sun: XCTestCase { //Step5: Check if output of sunUnderTest.date matches the expected output XCTAssertTrue(expectedDate == sunUnderTest.date) - } - - func testPerformance() throws { // Performance of setDate function that will refresh all the sun variables diff --git a/Tests/SunKitTests/UT_Utils.swift b/Tests/SunKitTests/UT_Utils.swift index 3e80aee..307a94c 100644 --- a/Tests/SunKitTests/UT_Utils.swift +++ b/Tests/SunKitTests/UT_Utils.swift @@ -21,21 +21,19 @@ import XCTest final class UT_Utils: XCTestCase { - /// Test of mod func testOfmod() throws { - - //Test1: -100 % 4 shall be equal to 4 + //Test1: -100 % 4 shall be equal to 4 //Step1: call function under test and check that it returns 4 var a = -100 var n = 8 XCTAssertTrue(4 == mod(a, n)) - //Test2: -400 % 360 shall be equal to 320 + //Test2: -400 % 360 shall be equal to 320 //Step1: call function under test and check that it returns 320 a = -400 n = 360 XCTAssertTrue(320 == mod(a, n)) - //Test3: 270 % 180 shall be equal to 90 + //Test3: 270 % 180 shall be equal to 90 //Step1: call function under test and check that it returns 90 a = 270 n = 180 @@ -43,9 +41,8 @@ final class UT_Utils: XCTestCase { } /// Test of jdFromDate - func testOfjdFromDate() throws{ - - //Test1: For 5/6/2010 at noon UT, his JD number shall be 2455323.0 + func testOfjdFromDate() throws { + //Test1: For 5/6/2010 at noon UT, his JD number shall be 2455323.0 //Step1: Creating UTC date let dateUnderTest = createDateUTC(day: 6, month: 5, year: 2010, hour: 12, minute: 0, seconds: 0) @@ -54,9 +51,8 @@ final class UT_Utils: XCTestCase { } /// Test of dateFromJD - func testOfdateFromJD() throws{ - - //Test1: 2455323.0 Jd number shall be quals to date 5/6/2010 at noon UT + func testOfdateFromJD() throws { + //Test1: 2455323.0 Jd number shall be quals to date 5/6/2010 at noon UT //Step1: Creating jd number under test let jdNumberTest = 2455323.0 @@ -64,46 +60,42 @@ final class UT_Utils: XCTestCase { let expectedOutput = createDateUTC(day: 6, month: 5, year: 2010, hour: 12, minute: 0, seconds: 0) XCTAssertTrue(expectedOutput == dateFromJd(jd: jdNumberTest)) } - + /// Test of extendMod func testOfExtendedMod() throws { - - //Test4: -270.8 % 180 shall be equal to 89.2 + //Test4: -270.8 % 180 shall be equal to 89.2 var a: Double = -270.8 var n = 180 - XCTAssertTrue(abs(89.2 - extendedMod(a,n)) < 0.1) - - //Test2: -0.8 % 1 shall be equal to 0.2 + + //Test2: -0.8 % 1 shall be equal to 0.2 a = -0.8 n = 1 - XCTAssertTrue(abs(0.2 - extendedMod(a,n)) < 0.1) - - //Test3: 390.5 % 360 shall be equal to 30.5 + + //Test3: 390.5 % 360 shall be equal to 30.5 a = 390.5 n = 360 - XCTAssertTrue(30.5 == extendedMod(a,n)) - - //Test4: 0.3 % 1 shall be equal to 0.3 + + //Test4: 0.3 % 1 shall be equal to 0.3 a = 0.3 n = 1 - XCTAssertTrue(0.3 == extendedMod(a,n)) - - //Test1: -100 % 4 shall be equal to 4 + + //Test1: -100 % 4 shall be equal to 4 //Step1: call function under test and check that it returns 4 a = -100 n = 8 - XCTAssertTrue(4 == extendedMod(a, n)) - //Test2: -400 % 360 shall be equal to 320 + + //Test2: -400 % 360 shall be equal to 320 //Step1: call function under test and check that it returns 320 a = -400 n = 360 XCTAssertTrue(320 == extendedMod(a, n)) - //Test3: 270 % 180 shall be equal to 90 + + //Test3: 270 % 180 shall be equal to 90 //Step1: call function under test and check that it returns 90 a = 270 n = 180 @@ -111,8 +103,7 @@ final class UT_Utils: XCTestCase { } /// Test of uT2GST - func testOfuT2GST() throws{ - + func testOfuT2GST() throws { //Test2: Convert 23h30m00s UT to GST for February 7, 2010. UseSameTimeZone equals False. //Step1: Creating 7/02/2010 23h30m UTC @@ -126,9 +117,8 @@ final class UT_Utils: XCTestCase { } /// Test of gST2LST - func testOfgST2LST() throws{ - - //Test1: Converting GST to LST requires knowing an observer’s longitude. Assume that the GST is 2h03m41s for an observer at 40° W longitude. + func testOfgST2LST() throws { + //Test1: Converting GST to LST requires knowing an observer’s longitude. Assume that the GST is 2h03m41s for an observer at 40° W longitude. //Step1: Creating 7/02/2010 2h03m41s with current time zone(i.e the one set on your device) let gstHMS: HMS = .init(hours: 2, minutes: 03, seconds: 41) @@ -139,10 +129,7 @@ final class UT_Utils: XCTestCase { //Step3: Call function under test and check that it returns a value which differs from expected output up to 0.01 let output = gST2LST(gstHMS, longitude: longitudeUnderTest).hMS2Decimal() - XCTAssert(abs(expectedOutput - output) < 0.01) + XCTAssert(abs(expectedOutput - output) < 0.01) } - - - }