could there be a scenario where the CoreLocation heading is larger
I'm not sure, but lots of angle-related math might take your value out of the 0..<360 range. To remedy this, you can add make something like:
extension Angle {
// Return a normalized copy of this angle that's guaranteed to be within +0 ..< +360
var normalized: Angle {
let potentiallyNegativeAngle = degrees.truncatingRemainder(dividingBy: 360.0)
let positiveInRangeAngle = (potentiallyNegativeAngle + 360).truncatingRemainder(dividingBy: 360.0)
return Angle(degrees: positiveInRangeAngle) }
}
Is there a built in method or better way to test for this?
No, but it can be pretty fun to write your own. Here's how I would do it:
// It astounds me that these basic operators aren't already built-in
extension Angle {
static func + (minuend: Angle, subtrahend: Angle) -> Angle {
Angle(radians: minuend.radians + subtrahend.radians)
}
static func - (minuend: Angle, subtrahend: Angle) -> Angle {
Angle(radians: minuend.radians - subtrahend.radians)
}
}
extension Angle {
// There's probably some clever way to do this without branching,
// and purely with modular arithmetic, but I couldn't figure it out
func isWithin(_ delta: Angle, of target: Angle) -> Bool {
return self.normalized > (target - delta).normalized ||
self.normalized < (target + delta).normalized
}
func isCloseToZero(delta: Angle = Angle(degrees: 2.0)) -> Bool {
isWithin(delta, of: Angle(degrees: 0))
}
}
Here are some test cases:
print(" -340.0: ", Angle(degrees: -340.0).isCloseToZero()) // False
print(" -358.1: ", Angle(degrees: -358.1).isCloseToZero())
print(" -5.0: ", Angle(degrees: -5.0).isCloseToZero()) // False
print(" -1.9: ", Angle(degrees: -1.9).isCloseToZero())
print(" 0.0: ", Angle(degrees: 0.0).isCloseToZero())
print(" +1.9: ", Angle(degrees: +1.9).isCloseToZero())
print(" +5.0: ", Angle(degrees: +5.0).isCloseToZero()) // False
print(" +358.1: ", Angle(degrees: +358.1).isCloseToZero())
print(" +360.0: ", Angle(degrees: +360.0).isCloseToZero())
print(" +365.0: ", Angle(degrees: +365.0).isCloseToZero()) // False
Here's a fancier variant, which does this totally branch-free using some clever modular arithmetic:
extension Angle {
// Returns the distance between `self` and `target`, in the range `-180..<180` degrees
func distance(to target: Angle) -> Angle {
let rawDistance = (self - target).radians
let normalizedDistance = .pi - abs(abs(rawDistance) - .pi)
return Angle(radians: normalizedDistance)
}
func isWithin(_ delta: Angle, of target: Angle) -> Bool {
let normalizedDelta = delta.normalized
precondition(normalizedDelta.radians <= .pi,
"""
`isWithin(_:of:)` always find the shortest distance between the two angles,
so the delta has to be congruent to an angle between 0 and 180 degrees!
It was \(delta), which normalized to: \(normalizedDelta)
"""
)
return abs(self.distance(to: target).radians) <= normalizedDelta.radians
}
func isCloseToZero(delta: Angle = Angle(degrees: 2.0)) -> Bool {
isWithin(delta, of: Angle(degrees: 0))
}
}