From b3132903932893b6cd0f7c555af7ac8240b62723 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Tue, 8 Dec 2020 15:35:37 +0100 Subject: [PATCH 01/22] Introduced Angle type [Issue #88] --- Sources/RealModule/Angle.swift | 92 ++++++++++++++++++++++++++++++++ Tests/RealTests/AngleTests.swift | 80 +++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 Sources/RealModule/Angle.swift create mode 100644 Tests/RealTests/AngleTests.swift diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift new file mode 100644 index 00000000..d7406ca5 --- /dev/null +++ b/Sources/RealModule/Angle.swift @@ -0,0 +1,92 @@ +//===--- Angle.swift ------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// A wrapper type for angle operations and functions +/// +/// All trigonometric functions expect the argument to be passed as radians (Real), but this is not enforced by the type system. +/// This type serves exactly this purpose, and can be seen as an alternative to the underlying Real implementation. +public struct Angle { + public var radians: T + public init(radians: T) { self.radians = radians } + public static func radians(_ val: T) -> Angle { .init(radians: val) } + + public var degrees: T { radians * 180 / .pi } + public init(degrees: T) { self.init(radians: degrees * .pi / 180) } + public static func degrees(_ val: T) -> Angle { .init(degrees: val) } +} + +public extension Angle { + /// See also: + /// - + /// `ElementaryFunctions.cosh()` + var cosh: T { T.cosh(radians) } + + /// See also: + /// - + /// `ElementaryFunctions.sinh()` + var sinh: T { T.sinh(radians) } + + /// See also: + /// - + /// `ElementaryFunctions.tanh()` + var tanh: T { T.tanh(radians) } + + /// See also: + /// - + /// `ElementaryFunctions.cos()` + var cos: T { T.cos(radians) } + + /// See also: + /// - + /// `ElementaryFunctions.sin()` + var sin: T { T.sin(radians) } + + /// See also: + /// - + /// `ElementaryFunctions.tan()` + var tan: T { T.tan(radians) } + + /// See also: + /// - + /// `ElementaryFunctions.acosh()` + static func acosh(_ x: T) -> Self { Angle.radians(T.acosh(x)) } + + /// See also: + /// - + /// `ElementaryFunctions.asinh()` + static func asinh(_ x: T) -> Self { Angle.radians(T.asinh(x)) } + + /// See also: + /// - + /// `ElementaryFunctions.atanh()` + static func atanh(_ x: T) -> Self { Angle.radians(T.atanh(x)) } + + /// See also: + /// - + /// `ElementaryFunctions.acos()` + static func acos(_ x: T) -> Self { Angle.radians(T.acos(x)) } + + /// See also: + /// - + /// `ElementaryFunctions.asin()` + static func asin(_ x: T) -> Self { Angle.radians(T.asin(x)) } + + /// See also: + /// - + /// `ElementaryFunctions.atan()` + static func atan(_ x: T) -> Self { Angle.radians(T.atan(x)) } + + /// See also: + /// - + /// `RealFunctions.atan2()` + static func atan2(y: T, x: T) -> Self { Angle.radians(T.atan2(y: y, x: x)) } +} diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift new file mode 100644 index 00000000..6c569790 --- /dev/null +++ b/Tests/RealTests/AngleTests.swift @@ -0,0 +1,80 @@ +//===--- AngleTests.swift -------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Numerics open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift Numerics project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import RealModule +import XCTest +import _TestSupport + +internal extension Real +where Self: BinaryFloatingPoint { + static func conversionBetweenRadiansAndDegreesChecks() { + let angleFromRadians = Angle(radians: Self.pi / 3) + assertClose(60, angleFromRadians.degrees) + + let angleFromDegrees = Angle(degrees: 120) + // the compiler complains with the following line + // assertClose(2 * Self.pi / 3, angleFromDegrees.radians) + assertClose(2 * Double(Self.pi) / 3, angleFromDegrees.radians) + } + + static func trigonometricFunctionChecks() { + assertClose(1.1863995522992575361931268186727044683, Angle.acos(0.375).radians) + assertClose(0.3843967744956390830381948729670469737, Angle.asin(0.375).radians) + assertClose(0.3587706702705722203959200639264604997, Angle.atan(0.375).radians) + assertClose(0.54041950027058415544357836460859991, Angle.atan2(y: 0.375, x: 0.625).radians) + + assertClose(0.9305076219123142911494767922295555080, Angle(radians: 0.375).cos) + assertClose(0.3662725290860475613729093517162641571, Angle(radians: 0.375).sin) + assertClose(0.3936265759256327582294137871012180981, Angle(radians: 0.375).tan) + } + + static func hyperbolicTrigonometricFunctionChecks() { + assertClose(0.4949329230945269058895630995767185785, Angle.acosh(1.125).radians) + assertClose(0.9670596312833237113713762009167286709, Angle.asinh(1.125).radians) + assertClose(0.7331685343967135223291211023213964500, Angle.atanh(0.625).radians) + assertClose(1.0711403467045867672994980155670160493, Angle(radians: 0.375).cosh) + assertClose(0.3838510679136145687542956764205024589, Angle(radians: 0.375).sinh) + assertClose(0.3583573983507859463193602315531580424, Angle(radians: 0.375).tanh) + } +} + +final class AngleTests: XCTestCase { + #if swift(>=5.3) && !(os(macOS) || os(iOS) && targetEnvironment(macCatalyst)) + func testFloat16() { + if #available(iOS 14.0, watchOS 14.0, tvOS 7.0, *) { + Float16.conversionBetweenRadiansAndDegreesChecks() + Float16.trigonometricFunctionChecks() + Float16.hyperbolicTrigonometricFunctionChecks() + } + } + #endif + + func testFloat() { + Float.conversionBetweenRadiansAndDegreesChecks() + Float.trigonometricFunctionChecks() + Float.hyperbolicTrigonometricFunctionChecks() + } + + func testDouble() { + Double.conversionBetweenRadiansAndDegreesChecks() + Double.trigonometricFunctionChecks() + Double.hyperbolicTrigonometricFunctionChecks() + } + + #if (arch(i386) || arch(x86_64)) && !os(Windows) && !os(Android) + func testFloat80() { + Float80.conversionBetweenRadiansAndDegreesChecks() + Float80.trigonometricFunctionChecks() + Float80.hyperbolicTrigonometricFunctionChecks() + } + #endif +} From c924790462466005044d866f07d5f685b08367cf Mon Sep 17 00:00:00 2001 From: jkaliak Date: Tue, 8 Dec 2020 22:40:50 +0100 Subject: [PATCH 02/22] Removed hyperbolic functions --- Sources/RealModule/Angle.swift | 32 +------------------------------- Tests/RealTests/AngleTests.swift | 13 ------------- 2 files changed, 1 insertion(+), 44 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index d7406ca5..b553e5f7 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -25,21 +25,6 @@ public struct Angle { } public extension Angle { - /// See also: - /// - - /// `ElementaryFunctions.cosh()` - var cosh: T { T.cosh(radians) } - - /// See also: - /// - - /// `ElementaryFunctions.sinh()` - var sinh: T { T.sinh(radians) } - - /// See also: - /// - - /// `ElementaryFunctions.tanh()` - var tanh: T { T.tanh(radians) } - /// See also: /// - /// `ElementaryFunctions.cos()` @@ -54,22 +39,7 @@ public extension Angle { /// - /// `ElementaryFunctions.tan()` var tan: T { T.tan(radians) } - - /// See also: - /// - - /// `ElementaryFunctions.acosh()` - static func acosh(_ x: T) -> Self { Angle.radians(T.acosh(x)) } - - /// See also: - /// - - /// `ElementaryFunctions.asinh()` - static func asinh(_ x: T) -> Self { Angle.radians(T.asinh(x)) } - - /// See also: - /// - - /// `ElementaryFunctions.atanh()` - static func atanh(_ x: T) -> Self { Angle.radians(T.atanh(x)) } - + /// See also: /// - /// `ElementaryFunctions.acos()` diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 6c569790..5f9a42dd 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -36,15 +36,6 @@ where Self: BinaryFloatingPoint { assertClose(0.3662725290860475613729093517162641571, Angle(radians: 0.375).sin) assertClose(0.3936265759256327582294137871012180981, Angle(radians: 0.375).tan) } - - static func hyperbolicTrigonometricFunctionChecks() { - assertClose(0.4949329230945269058895630995767185785, Angle.acosh(1.125).radians) - assertClose(0.9670596312833237113713762009167286709, Angle.asinh(1.125).radians) - assertClose(0.7331685343967135223291211023213964500, Angle.atanh(0.625).radians) - assertClose(1.0711403467045867672994980155670160493, Angle(radians: 0.375).cosh) - assertClose(0.3838510679136145687542956764205024589, Angle(radians: 0.375).sinh) - assertClose(0.3583573983507859463193602315531580424, Angle(radians: 0.375).tanh) - } } final class AngleTests: XCTestCase { @@ -53,7 +44,6 @@ final class AngleTests: XCTestCase { if #available(iOS 14.0, watchOS 14.0, tvOS 7.0, *) { Float16.conversionBetweenRadiansAndDegreesChecks() Float16.trigonometricFunctionChecks() - Float16.hyperbolicTrigonometricFunctionChecks() } } #endif @@ -61,20 +51,17 @@ final class AngleTests: XCTestCase { func testFloat() { Float.conversionBetweenRadiansAndDegreesChecks() Float.trigonometricFunctionChecks() - Float.hyperbolicTrigonometricFunctionChecks() } func testDouble() { Double.conversionBetweenRadiansAndDegreesChecks() Double.trigonometricFunctionChecks() - Double.hyperbolicTrigonometricFunctionChecks() } #if (arch(i386) || arch(x86_64)) && !os(Windows) && !os(Android) func testFloat80() { Float80.conversionBetweenRadiansAndDegreesChecks() Float80.trigonometricFunctionChecks() - Float80.hyperbolicTrigonometricFunctionChecks() } #endif } From 437b2809d86f8e03a71695c8b44ca36a791363f9 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Wed, 9 Dec 2020 01:51:06 +0100 Subject: [PATCH 03/22] cos(Angle) changed, 270deg stil not giving 0 --- Sources/RealModule/Angle.swift | 45 ++++++++++++++++++++++++++---- Tests/RealTests/AngleTests.swift | 48 ++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index b553e5f7..fa3e96b8 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -18,28 +18,61 @@ public struct Angle { public var radians: T public init(radians: T) { self.radians = radians } public static func radians(_ val: T) -> Angle { .init(radians: val) } - + public var degrees: T { radians * 180 / .pi } public init(degrees: T) { self.init(radians: degrees * .pi / 180) } public static func degrees(_ val: T) -> Angle { .init(degrees: val) } } -public extension Angle { +public extension ElementaryFunctions +where Self: Real { /// See also: /// - /// `ElementaryFunctions.cos()` - var cos: T { T.cos(radians) } + static func cos(_ angle: Angle) -> Self { + let normalizedRadians = normalized(angle.radians) + + if -.pi/4 < normalizedRadians && normalizedRadians < .pi/4 { + return Self.cos(normalizedRadians) + } + + if normalizedRadians > 3 * .pi / 4 || normalizedRadians < -3 * .pi / 4 { + return -Self.cos(.pi - normalizedRadians) + } + + if normalizedRadians >= 0 { + return Self.sin(.pi/2 - normalizedRadians) + } + + return Self.sin(normalizedRadians + .pi / 2) + } /// See also: /// - /// `ElementaryFunctions.sin()` - var sin: T { T.sin(radians) } + static func sin(_ angle: Angle) -> Self { Self.sin(angle.radians) } /// See also: /// - /// `ElementaryFunctions.tan()` - var tan: T { T.tan(radians) } - + static func tan(_ angle: Angle) -> Self { Self.tan(angle.radians) } + + private static func normalized(_ radians: Self) -> Self { + var normalizedRadians = radians + + while normalizedRadians > Self.pi { + normalizedRadians -= 2 * Self.pi + } + + while normalizedRadians < -Self.pi { + normalizedRadians += 2 * Self.pi + } + + return normalizedRadians + } +} + +public extension Angle { /// See also: /// - /// `ElementaryFunctions.acos()` diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 5f9a42dd..dd8f56c8 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -31,10 +31,48 @@ where Self: BinaryFloatingPoint { assertClose(0.3843967744956390830381948729670469737, Angle.asin(0.375).radians) assertClose(0.3587706702705722203959200639264604997, Angle.atan(0.375).radians) assertClose(0.54041950027058415544357836460859991, Angle.atan2(y: 0.375, x: 0.625).radians) + + assertClose(0.9305076219123142911494767922295555080, cos(Angle(radians: 0.375))) + assertClose(0.3662725290860475613729093517162641571, sin(Angle(radians: 0.375))) + assertClose(0.3936265759256327582294137871012180981, tan(Angle(radians: 0.375))) + } + + static func specialDegreesTrigonometricFunctionChecks() { + assertClose(1, cos(Angle(degrees: 0))) + assertClose(0.86602540378443864676372317075293618347, cos(Angle(degrees: 30))) + assertClose(0.70710678118654752440084436210484903929, cos(Angle(degrees: 45))) + assertClose(0.5, cos(Angle(degrees: 60))) + assertClose(0, cos(Angle(degrees: 90))) + assertClose(-0.5, cos(Angle(degrees: 120))) + assertClose(-0.70710678118654752440084436210484903929, cos(Angle(degrees: 135))) + assertClose(-0.86602540378443864676372317075293618347, cos(Angle(degrees: 150))) + assertClose(-1, cos(Angle(degrees: 180))) + assertClose(0, cos(Angle(degrees: 270))) + assertClose(0, cos(Angle(degrees: -90))) + assertClose(-1, cos(Angle(degrees: -180))) + + assertClose(0, sin(Angle(degrees: 0))) + assertClose(0.5, sin(Angle(degrees: 30))) + assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 45))) + assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 60))) + assertClose(1, sin(Angle(degrees: 90))) + assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 120))) + assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 135))) + assertClose(0.5, sin(Angle(degrees: 150))) + assertClose(0, sin(Angle(degrees: 180))) + assertClose(-1, sin(Angle(degrees: 270))) + assertClose(-1, sin(Angle(degrees: -90))) + assertClose(0, sin(Angle(degrees: -180))) - assertClose(0.9305076219123142911494767922295555080, Angle(radians: 0.375).cos) - assertClose(0.3662725290860475613729093517162641571, Angle(radians: 0.375).sin) - assertClose(0.3936265759256327582294137871012180981, Angle(radians: 0.375).tan) + assertClose(0, tan(Angle(degrees: 0))) + assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) + assertClose(1, tan(Angle(degrees: 45))) + assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: 60))) + assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: 120))) + assertClose(-1, tan(Angle(degrees: 135))) + assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: 150))) + assertClose(0, tan(Angle(degrees: 180))) + assertClose(0, tan(Angle(degrees: -180))) } } @@ -44,6 +82,7 @@ final class AngleTests: XCTestCase { if #available(iOS 14.0, watchOS 14.0, tvOS 7.0, *) { Float16.conversionBetweenRadiansAndDegreesChecks() Float16.trigonometricFunctionChecks() + Float16.specialDegreesTrigonometricFunctionChecks() } } #endif @@ -51,17 +90,20 @@ final class AngleTests: XCTestCase { func testFloat() { Float.conversionBetweenRadiansAndDegreesChecks() Float.trigonometricFunctionChecks() + Float.specialDegreesTrigonometricFunctionChecks() } func testDouble() { Double.conversionBetweenRadiansAndDegreesChecks() Double.trigonometricFunctionChecks() + Double.specialDegreesTrigonometricFunctionChecks() } #if (arch(i386) || arch(x86_64)) && !os(Windows) && !os(Android) func testFloat80() { Float80.conversionBetweenRadiansAndDegreesChecks() Float80.trigonometricFunctionChecks() + Float80.specialDegreesTrigonometricFunctionChecks() } #endif } From bb198095ca33ae06a74e3f906ab030dc0b5f3fa3 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Wed, 9 Dec 2020 02:33:07 +0100 Subject: [PATCH 04/22] cos works for all cases --- Sources/RealModule/Angle.swift | 40 +++++++++++++++------------ Tests/RealTests/AngleTests.swift | 47 +++++++++++++++++--------------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index fa3e96b8..0f0e98a9 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -14,23 +14,26 @@ /// /// All trigonometric functions expect the argument to be passed as radians (Real), but this is not enforced by the type system. /// This type serves exactly this purpose, and can be seen as an alternative to the underlying Real implementation. -public struct Angle { +public struct Angle { public var radians: T public init(radians: T) { self.radians = radians } public static func radians(_ val: T) -> Angle { .init(radians: val) } public var degrees: T { radians * 180 / .pi } - public init(degrees: T) { self.init(radians: degrees * .pi / 180) } + public init(degrees: T) { + let normalized = normalize(degrees, limit: 180) + self.init(radians: normalized * .pi / 180) + } public static func degrees(_ val: T) -> Angle { .init(degrees: val) } } public extension ElementaryFunctions -where Self: Real { +where Self: Real & BinaryFloatingPoint { /// See also: /// - /// `ElementaryFunctions.cos()` static func cos(_ angle: Angle) -> Self { - let normalizedRadians = normalized(angle.radians) + let normalizedRadians = normalize(angle.radians, limit: .pi) if -.pi/4 < normalizedRadians && normalizedRadians < .pi/4 { return Self.cos(normalizedRadians) @@ -56,20 +59,6 @@ where Self: Real { /// - /// `ElementaryFunctions.tan()` static func tan(_ angle: Angle) -> Self { Self.tan(angle.radians) } - - private static func normalized(_ radians: Self) -> Self { - var normalizedRadians = radians - - while normalizedRadians > Self.pi { - normalizedRadians -= 2 * Self.pi - } - - while normalizedRadians < -Self.pi { - normalizedRadians += 2 * Self.pi - } - - return normalizedRadians - } } public extension Angle { @@ -93,3 +82,18 @@ public extension Angle { /// `RealFunctions.atan2()` static func atan2(y: T, x: T) -> Self { Angle.radians(T.atan2(y: y, x: x)) } } + +private func normalize(_ input: T, limit: T) -> T +where T: Real & BinaryFloatingPoint { + var normalized = input + + while normalized > limit { + normalized -= 2 * limit + } + + while normalized < -limit { + normalized += 2 * limit + } + + return normalized +} diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index dd8f56c8..9714e575 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -48,31 +48,34 @@ where Self: BinaryFloatingPoint { assertClose(-0.86602540378443864676372317075293618347, cos(Angle(degrees: 150))) assertClose(-1, cos(Angle(degrees: 180))) assertClose(0, cos(Angle(degrees: 270))) + assertClose(1, cos(Angle(degrees: 360))) assertClose(0, cos(Angle(degrees: -90))) assertClose(-1, cos(Angle(degrees: -180))) - assertClose(0, sin(Angle(degrees: 0))) - assertClose(0.5, sin(Angle(degrees: 30))) - assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 45))) - assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 60))) - assertClose(1, sin(Angle(degrees: 90))) - assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 120))) - assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 135))) - assertClose(0.5, sin(Angle(degrees: 150))) - assertClose(0, sin(Angle(degrees: 180))) - assertClose(-1, sin(Angle(degrees: 270))) - assertClose(-1, sin(Angle(degrees: -90))) - assertClose(0, sin(Angle(degrees: -180))) - - assertClose(0, tan(Angle(degrees: 0))) - assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) - assertClose(1, tan(Angle(degrees: 45))) - assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: 60))) - assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: 120))) - assertClose(-1, tan(Angle(degrees: 135))) - assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: 150))) - assertClose(0, tan(Angle(degrees: 180))) - assertClose(0, tan(Angle(degrees: -180))) +// assertClose(0, sin(Angle(degrees: 0))) +// assertClose(0.5, sin(Angle(degrees: 30))) +// assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 45))) +// assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 60))) +// assertClose(1, sin(Angle(degrees: 90))) +// assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 120))) +// assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 135))) +// assertClose(0.5, sin(Angle(degrees: 150))) +// assertClose(0, sin(Angle(degrees: 180))) +// assertClose(-1, sin(Angle(degrees: 270))) +// assertClose(0, sin(Angle(degrees: 360))) +// assertClose(-1, sin(Angle(degrees: -90))) +// assertClose(0, sin(Angle(degrees: -180))) +// +// assertClose(0, tan(Angle(degrees: 0))) +// assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) +// assertClose(1, tan(Angle(degrees: 45))) +// assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: 60))) +// assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: 120))) +// assertClose(-1, tan(Angle(degrees: 135))) +// assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: 150))) +// assertClose(0, tan(Angle(degrees: 180))) +// assertClose(0, tan(Angle(degrees: 360))) +// assertClose(0, tan(Angle(degrees: -180))) } } From d4a4c3ce1132e521cd4608633e4595b4a13f7a1c Mon Sep 17 00:00:00 2001 From: jkaliak Date: Wed, 9 Dec 2020 18:50:48 +0100 Subject: [PATCH 05/22] sin(Angle) also works as expected --- Sources/RealModule/Angle.swift | 23 ++++++++++++++- Tests/RealTests/AngleTests.swift | 28 +++++++++---------- .../RealTests/ElementaryFunctionChecks.swift | 8 ++++-- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index 0f0e98a9..30b4314f 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -53,7 +53,28 @@ where Self: Real & BinaryFloatingPoint { /// See also: /// - /// `ElementaryFunctions.sin()` - static func sin(_ angle: Angle) -> Self { Self.sin(angle.radians) } + static func sin(_ angle: Angle) -> Self { + let normalizedRadians = normalize(angle.radians, limit: .pi) + + if .pi / 4 < normalizedRadians && normalizedRadians < 3 * .pi / 4 { + return Self.sin(normalizedRadians) + } + + if -3 * .pi / 4 < normalizedRadians && normalizedRadians < -.pi / 4 { + return -Self.sin(-normalizedRadians) + } + + if normalizedRadians > 3 * .pi / 4 { + return Self.sin(.pi - normalizedRadians) + } + + if normalizedRadians < -3 * .pi / 4 { + return -Self.sin(.pi + normalizedRadians) + } + + + return Self.sin(normalizedRadians) + } /// See also: /// - diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 9714e575..6d788818 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -52,20 +52,20 @@ where Self: BinaryFloatingPoint { assertClose(0, cos(Angle(degrees: -90))) assertClose(-1, cos(Angle(degrees: -180))) -// assertClose(0, sin(Angle(degrees: 0))) -// assertClose(0.5, sin(Angle(degrees: 30))) -// assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 45))) -// assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 60))) -// assertClose(1, sin(Angle(degrees: 90))) -// assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 120))) -// assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 135))) -// assertClose(0.5, sin(Angle(degrees: 150))) -// assertClose(0, sin(Angle(degrees: 180))) -// assertClose(-1, sin(Angle(degrees: 270))) -// assertClose(0, sin(Angle(degrees: 360))) -// assertClose(-1, sin(Angle(degrees: -90))) -// assertClose(0, sin(Angle(degrees: -180))) -// + assertClose(0, sin(Angle(degrees: 0))) + assertClose(0.5, sin(Angle(degrees: 30))) + assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 45))) + assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 60))) + assertClose(1, sin(Angle(degrees: 90))) + assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 120))) + assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 135))) + assertClose(0.5, sin(Angle(degrees: 150))) + assertClose(0, sin(Angle(degrees: 180))) + assertClose(-1, sin(Angle(degrees: 270))) + assertClose(0, sin(Angle(degrees: 360))) + assertClose(-1, sin(Angle(degrees: -90))) + assertClose(0, sin(Angle(degrees: -180))) +// // assertClose(0, tan(Angle(degrees: 0))) // assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) // assertClose(1, tan(Angle(degrees: 45))) diff --git a/Tests/RealTests/ElementaryFunctionChecks.swift b/Tests/RealTests/ElementaryFunctionChecks.swift index a1c0ea8a..b3a37042 100644 --- a/Tests/RealTests/ElementaryFunctionChecks.swift +++ b/Tests/RealTests/ElementaryFunctionChecks.swift @@ -27,6 +27,11 @@ internal func assertClose( file: StaticString = #file, line: UInt = #line ) -> T where T: BinaryFloatingPoint { + // we need to first check if the values are zero before we check the signs + // otherwise, "0" and "-0" compare as unequal (eg. sin(-180) == 0) + let expectedT = T(expected) + if observed.isZero && expectedT.isZero { return 0 } + // Shortcut relative-error check if we got the sign wrong; it's OK to // underflow to zero, but we do not want to allow going right through // zero and getting the sign wrong. @@ -38,8 +43,7 @@ internal func assertClose( if observed.isNaN && expected.isNaN { return 0 } // If T(expected) is zero or infinite, and matches observed, the error // is zero. - let expectedT = T(expected) - if observed.isZero && expectedT.isZero { return 0 } + if observed.isInfinite && expectedT.isInfinite { return 0 } // Special-case where only one of expectedT or observed is infinity. // Artificially knock everything down a binade, treat actual infinity as From f676bc1892ccb05ed86ca86ef43dca37dba8f37d Mon Sep 17 00:00:00 2001 From: jkaliak Date: Wed, 9 Dec 2020 18:50:59 +0100 Subject: [PATCH 06/22] removed newline --- Sources/RealModule/Angle.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index 30b4314f..409bd416 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -72,7 +72,6 @@ where Self: Real & BinaryFloatingPoint { return -Self.sin(.pi + normalizedRadians) } - return Self.sin(normalizedRadians) } From bb474f5983c7c5a47c3aee6c15735a57361675e1 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Wed, 9 Dec 2020 18:53:07 +0100 Subject: [PATCH 07/22] more tests for cos and sin --- Tests/RealTests/AngleTests.swift | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 6d788818..77fdf2ac 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -38,6 +38,10 @@ where Self: BinaryFloatingPoint { } static func specialDegreesTrigonometricFunctionChecks() { + assertClose(1, cos(Angle(degrees: -360))) + assertClose(0, cos(Angle(degrees: -270))) + assertClose(-1, cos(Angle(degrees: -180))) + assertClose(0, cos(Angle(degrees: -90))) assertClose(1, cos(Angle(degrees: 0))) assertClose(0.86602540378443864676372317075293618347, cos(Angle(degrees: 30))) assertClose(0.70710678118654752440084436210484903929, cos(Angle(degrees: 45))) @@ -49,9 +53,11 @@ where Self: BinaryFloatingPoint { assertClose(-1, cos(Angle(degrees: 180))) assertClose(0, cos(Angle(degrees: 270))) assertClose(1, cos(Angle(degrees: 360))) - assertClose(0, cos(Angle(degrees: -90))) - assertClose(-1, cos(Angle(degrees: -180))) + assertClose(0, sin(Angle(degrees: -360))) + assertClose(1, sin(Angle(degrees: -270))) + assertClose(0, sin(Angle(degrees: -180))) + assertClose(-1, sin(Angle(degrees: -90))) assertClose(0, sin(Angle(degrees: 0))) assertClose(0.5, sin(Angle(degrees: 30))) assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 45))) @@ -63,8 +69,6 @@ where Self: BinaryFloatingPoint { assertClose(0, sin(Angle(degrees: 180))) assertClose(-1, sin(Angle(degrees: 270))) assertClose(0, sin(Angle(degrees: 360))) - assertClose(-1, sin(Angle(degrees: -90))) - assertClose(0, sin(Angle(degrees: -180))) // // assertClose(0, tan(Angle(degrees: 0))) // assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) From 2fad678cf0e6554b66c77a8773923f972a33f9e7 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Wed, 9 Dec 2020 19:00:54 +0100 Subject: [PATCH 08/22] =?UTF-8?q?cos=20and=20sin=20tests=20along=20the=20w?= =?UTF-8?q?hole=20(-=CF=80,=CF=80)=20interval?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Tests/RealTests/AngleTests.swift | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 77fdf2ac..4065c616 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -41,7 +41,13 @@ where Self: BinaryFloatingPoint { assertClose(1, cos(Angle(degrees: -360))) assertClose(0, cos(Angle(degrees: -270))) assertClose(-1, cos(Angle(degrees: -180))) + assertClose(-0.86602540378443864676372317075293618347, cos(Angle(degrees: -150))) + assertClose(-0.70710678118654752440084436210484903929, cos(Angle(degrees: -135))) + assertClose(-0.5, cos(Angle(degrees: -120))) assertClose(0, cos(Angle(degrees: -90))) + assertClose(0.5, cos(Angle(degrees: -60))) + assertClose(0.70710678118654752440084436210484903929, cos(Angle(degrees: -45))) + assertClose(0.86602540378443864676372317075293618347, cos(Angle(degrees: -30))) assertClose(1, cos(Angle(degrees: 0))) assertClose(0.86602540378443864676372317075293618347, cos(Angle(degrees: 30))) assertClose(0.70710678118654752440084436210484903929, cos(Angle(degrees: 45))) @@ -57,7 +63,13 @@ where Self: BinaryFloatingPoint { assertClose(0, sin(Angle(degrees: -360))) assertClose(1, sin(Angle(degrees: -270))) assertClose(0, sin(Angle(degrees: -180))) + assertClose(-0.5, sin(Angle(degrees: -150))) + assertClose(-0.70710678118654752440084436210484903929, sin(Angle(degrees: -135))) + assertClose(-0.86602540378443864676372317075293618347, sin(Angle(degrees: -120))) assertClose(-1, sin(Angle(degrees: -90))) + assertClose(-0.86602540378443864676372317075293618347, sin(Angle(degrees: -60))) + assertClose(-0.70710678118654752440084436210484903929, sin(Angle(degrees: -45))) + assertClose(-0.5, sin(Angle(degrees: -30))) assertClose(0, sin(Angle(degrees: 0))) assertClose(0.5, sin(Angle(degrees: 30))) assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 45))) @@ -69,7 +81,11 @@ where Self: BinaryFloatingPoint { assertClose(0, sin(Angle(degrees: 180))) assertClose(-1, sin(Angle(degrees: 270))) assertClose(0, sin(Angle(degrees: 360))) -// + +// let t = Self.tan(.pi/2) +// let isInfinite = t.isInfinite +// assertClose(0, tan(Angle(degrees: -360))) +// assertClose(0, tan(Angle(degrees: -180))) // assertClose(0, tan(Angle(degrees: 0))) // assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) // assertClose(1, tan(Angle(degrees: 45))) @@ -79,7 +95,7 @@ where Self: BinaryFloatingPoint { // assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: 150))) // assertClose(0, tan(Angle(degrees: 180))) // assertClose(0, tan(Angle(degrees: 360))) -// assertClose(0, tan(Angle(degrees: -180))) + } } From 4844b2aeeffec3fa3129c7d0236222a76e8d8c7b Mon Sep 17 00:00:00 2001 From: jkaliak Date: Wed, 9 Dec 2020 19:18:02 +0100 Subject: [PATCH 09/22] tests and implementation for tan --- Sources/RealModule/Angle.swift | 15 +++++++++++++- Tests/RealTests/AngleTests.swift | 34 ++++++++++++++++++++------------ 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index 409bd416..a149ced6 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -78,7 +78,20 @@ where Self: Real & BinaryFloatingPoint { /// See also: /// - /// `ElementaryFunctions.tan()` - static func tan(_ angle: Angle) -> Self { Self.tan(angle.radians) } + static func tan(_ angle: Angle) -> Self { + let sine = sin(angle) + let cosine = cos(angle) + + guard cosine != 0 else { + var result = Self.infinity + if sine.sign == .minus { + result.negate() + } + return result + } + + return sine / cosine + } } public extension Angle { diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 4065c616..ba52ca19 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -82,19 +82,27 @@ where Self: BinaryFloatingPoint { assertClose(-1, sin(Angle(degrees: 270))) assertClose(0, sin(Angle(degrees: 360))) -// let t = Self.tan(.pi/2) -// let isInfinite = t.isInfinite -// assertClose(0, tan(Angle(degrees: -360))) -// assertClose(0, tan(Angle(degrees: -180))) -// assertClose(0, tan(Angle(degrees: 0))) -// assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) -// assertClose(1, tan(Angle(degrees: 45))) -// assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: 60))) -// assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: 120))) -// assertClose(-1, tan(Angle(degrees: 135))) -// assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: 150))) -// assertClose(0, tan(Angle(degrees: 180))) -// assertClose(0, tan(Angle(degrees: 360))) + assertClose(0, tan(Angle(degrees: -360))) + assertClose(Double.infinity, tan(Angle(degrees: -270))) + assertClose(0, tan(Angle(degrees: -180))) + assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: -150))) + assertClose(1, tan(Angle(degrees: -135))) + assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: -120))) + assertClose(-Double.infinity, tan(Angle(degrees: -90))) + assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: -60))) + assertClose(-1, tan(Angle(degrees: -45))) + assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: -30))) + assertClose(0, tan(Angle(degrees: 0))) + assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) + assertClose(1, tan(Angle(degrees: 45))) + assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: 60))) + assertClose(Double.infinity, tan(Angle(degrees: 90))) + assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: 120))) + assertClose(-1, tan(Angle(degrees: 135))) + assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: 150))) + assertClose(0, tan(Angle(degrees: 180))) + assertClose(-Double.infinity, tan(Angle(degrees: 270))) + assertClose(0, tan(Angle(degrees: 360))) } } From c5828b83bef670e1246fa7087bdb2cba39535a68 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Wed, 9 Dec 2020 21:49:51 +0100 Subject: [PATCH 10/22] Strict equality in tests, not approximate The tangent tests are not passing, this should be fixed. --- Tests/RealTests/AngleTests.swift | 62 ++++++++++++++++---------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index ba52ca19..18df4212 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -38,71 +38,71 @@ where Self: BinaryFloatingPoint { } static func specialDegreesTrigonometricFunctionChecks() { - assertClose(1, cos(Angle(degrees: -360))) - assertClose(0, cos(Angle(degrees: -270))) - assertClose(-1, cos(Angle(degrees: -180))) + XCTAssertEqual(1, cos(Angle(degrees: -360))) + XCTAssertEqual(0, cos(Angle(degrees: -270))) + XCTAssertEqual(-1, cos(Angle(degrees: -180))) assertClose(-0.86602540378443864676372317075293618347, cos(Angle(degrees: -150))) assertClose(-0.70710678118654752440084436210484903929, cos(Angle(degrees: -135))) assertClose(-0.5, cos(Angle(degrees: -120))) - assertClose(0, cos(Angle(degrees: -90))) + XCTAssertEqual(0, cos(Angle(degrees: -90))) assertClose(0.5, cos(Angle(degrees: -60))) assertClose(0.70710678118654752440084436210484903929, cos(Angle(degrees: -45))) assertClose(0.86602540378443864676372317075293618347, cos(Angle(degrees: -30))) - assertClose(1, cos(Angle(degrees: 0))) + XCTAssertEqual(1, cos(Angle(degrees: 0))) assertClose(0.86602540378443864676372317075293618347, cos(Angle(degrees: 30))) assertClose(0.70710678118654752440084436210484903929, cos(Angle(degrees: 45))) assertClose(0.5, cos(Angle(degrees: 60))) - assertClose(0, cos(Angle(degrees: 90))) + XCTAssertEqual(0, cos(Angle(degrees: 90))) assertClose(-0.5, cos(Angle(degrees: 120))) assertClose(-0.70710678118654752440084436210484903929, cos(Angle(degrees: 135))) assertClose(-0.86602540378443864676372317075293618347, cos(Angle(degrees: 150))) - assertClose(-1, cos(Angle(degrees: 180))) - assertClose(0, cos(Angle(degrees: 270))) - assertClose(1, cos(Angle(degrees: 360))) + XCTAssertEqual(-1, cos(Angle(degrees: 180))) + XCTAssertEqual(0, cos(Angle(degrees: 270))) + XCTAssertEqual(1, cos(Angle(degrees: 360))) - assertClose(0, sin(Angle(degrees: -360))) - assertClose(1, sin(Angle(degrees: -270))) - assertClose(0, sin(Angle(degrees: -180))) + XCTAssertEqual(0, sin(Angle(degrees: -360))) + XCTAssertEqual(1, sin(Angle(degrees: -270))) + XCTAssertEqual(0, sin(Angle(degrees: -180))) assertClose(-0.5, sin(Angle(degrees: -150))) assertClose(-0.70710678118654752440084436210484903929, sin(Angle(degrees: -135))) assertClose(-0.86602540378443864676372317075293618347, sin(Angle(degrees: -120))) - assertClose(-1, sin(Angle(degrees: -90))) + XCTAssertEqual(-1, sin(Angle(degrees: -90))) assertClose(-0.86602540378443864676372317075293618347, sin(Angle(degrees: -60))) assertClose(-0.70710678118654752440084436210484903929, sin(Angle(degrees: -45))) assertClose(-0.5, sin(Angle(degrees: -30))) - assertClose(0, sin(Angle(degrees: 0))) + XCTAssertEqual(0, sin(Angle(degrees: 0))) assertClose(0.5, sin(Angle(degrees: 30))) assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 45))) assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 60))) - assertClose(1, sin(Angle(degrees: 90))) + XCTAssertEqual(1, sin(Angle(degrees: 90))) assertClose(0.86602540378443864676372317075293618347, sin(Angle(degrees: 120))) assertClose(0.70710678118654752440084436210484903929, sin(Angle(degrees: 135))) assertClose(0.5, sin(Angle(degrees: 150))) - assertClose(0, sin(Angle(degrees: 180))) - assertClose(-1, sin(Angle(degrees: 270))) - assertClose(0, sin(Angle(degrees: 360))) + XCTAssertEqual(0, sin(Angle(degrees: 180))) + XCTAssertEqual(-1, sin(Angle(degrees: 270))) + XCTAssertEqual(0, sin(Angle(degrees: 360))) - assertClose(0, tan(Angle(degrees: -360))) - assertClose(Double.infinity, tan(Angle(degrees: -270))) - assertClose(0, tan(Angle(degrees: -180))) + XCTAssertEqual(0, tan(Angle(degrees: -360))) + XCTAssertEqual(.infinity, tan(Angle(degrees: -270))) + XCTAssertEqual(0, tan(Angle(degrees: -180))) assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: -150))) - assertClose(1, tan(Angle(degrees: -135))) + XCTAssertEqual(1, tan(Angle(degrees: -135))) assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: -120))) - assertClose(-Double.infinity, tan(Angle(degrees: -90))) + XCTAssertEqual(-.infinity, tan(Angle(degrees: -90))) assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: -60))) - assertClose(-1, tan(Angle(degrees: -45))) + XCTAssertEqual(-1, tan(Angle(degrees: -45))) assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: -30))) - assertClose(0, tan(Angle(degrees: 0))) + XCTAssertEqual(0, tan(Angle(degrees: 0))) assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) - assertClose(1, tan(Angle(degrees: 45))) + XCTAssertEqual(1, tan(Angle(degrees: 45))) assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: 60))) - assertClose(Double.infinity, tan(Angle(degrees: 90))) + XCTAssertEqual(.infinity, tan(Angle(degrees: 90))) assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: 120))) - assertClose(-1, tan(Angle(degrees: 135))) + XCTAssertEqual(-1, tan(Angle(degrees: 135))) assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: 150))) - assertClose(0, tan(Angle(degrees: 180))) - assertClose(-Double.infinity, tan(Angle(degrees: 270))) - assertClose(0, tan(Angle(degrees: 360))) + XCTAssertEqual(0, tan(Angle(degrees: 180))) + XCTAssertEqual(-.infinity, tan(Angle(degrees: 270))) + XCTAssertEqual(0, tan(Angle(degrees: 360))) } } From 2fba7c72e8c114ae59a67f401525c5795a246449 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 13 Dec 2020 23:55:36 +0100 Subject: [PATCH 11/22] Removed conformance to BinaryFloatingPoint --- Sources/RealModule/Angle.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index a149ced6..4ccb99dc 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -14,7 +14,7 @@ /// /// All trigonometric functions expect the argument to be passed as radians (Real), but this is not enforced by the type system. /// This type serves exactly this purpose, and can be seen as an alternative to the underlying Real implementation. -public struct Angle { +public struct Angle { public var radians: T public init(radians: T) { self.radians = radians } public static func radians(_ val: T) -> Angle { .init(radians: val) } @@ -28,7 +28,7 @@ public struct Angle { } public extension ElementaryFunctions -where Self: Real & BinaryFloatingPoint { +where Self: Real { /// See also: /// - /// `ElementaryFunctions.cos()` @@ -117,7 +117,7 @@ public extension Angle { } private func normalize(_ input: T, limit: T) -> T -where T: Real & BinaryFloatingPoint { +where T: Real { var normalized = input while normalized > limit { From 79f5f7a11349687b2defa9b76c52b939f88b0953 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Mon, 14 Dec 2020 00:10:23 +0100 Subject: [PATCH 12/22] Added conformance to AdditiveArithmetic and other utilities --- Sources/RealModule/Angle.swift | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index 4ccb99dc..6c9424c9 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -116,6 +116,50 @@ public extension Angle { static func atan2(y: T, x: T) -> Self { Angle.radians(T.atan2(y: y, x: x)) } } +extension Angle: AdditiveArithmetic { + public static var zero: Angle { .radians(0) } + + public static func + (lhs: Angle, rhs: Angle) -> Angle { + Angle(radians: lhs.radians + rhs.radians) + } + + public static func += (lhs: inout Angle, rhs: Angle) { + lhs.radians += rhs.radians + } + + public static func - (lhs: Angle, rhs: Angle) -> Angle { + Angle(radians: lhs.radians - rhs.radians) + } + + public static func -= (lhs: inout Angle, rhs: Angle) { + lhs.radians -= rhs.radians + } +} + +public extension Angle { + static func * (lhs: Angle, rhs: T) -> Angle { + Angle(radians: lhs.radians * rhs) + } + + static func *= (lhs: inout Angle, rhs: T) { + lhs.radians *= rhs + } + + static func * (lhs: T, rhs: Angle) -> Angle { + Angle(radians: rhs.radians * lhs) + } + + static func / (lhs: Angle, rhs: T) -> Angle { + assert(rhs != 0) + return Angle(radians: lhs.radians / rhs) + } + + static func /= (lhs: inout Angle, rhs: T) { + assert(rhs != 0) + lhs.radians /= rhs + } +} + private func normalize(_ input: T, limit: T) -> T where T: Real { var normalized = input From 5f57416cba78dcf801290215e2a57459f3118304 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Mon, 14 Dec 2020 00:29:05 +0100 Subject: [PATCH 13/22] Added AdditiveArithmetic tests --- Sources/RealModule/Angle.swift | 6 ++++++ Tests/RealTests/AngleTests.swift | 22 +++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index 6c9424c9..7ebe3e60 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -160,6 +160,12 @@ public extension Angle { } } +extension Angle: Equatable { + public static func == (lhs: Angle, rhs: Angle) -> Bool { + lhs.radians.isApproximatelyEqual(to: rhs.radians) + } +} + private func normalize(_ input: T, limit: T) -> T where T: Real { var normalized = input diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 18df4212..ddacd7c4 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -103,7 +103,23 @@ where Self: BinaryFloatingPoint { XCTAssertEqual(0, tan(Angle(degrees: 180))) XCTAssertEqual(-.infinity, tan(Angle(degrees: 270))) XCTAssertEqual(0, tan(Angle(degrees: 360))) - + } + + static func additiveArithmeticTests() { + var angle = Angle(degrees: 30) + XCTAssertEqual(Angle(degrees: 50), angle + Angle(degrees: 20)) + XCTAssertEqual(Angle(degrees: 10), angle - Angle(degrees: 20)) + XCTAssertEqual(Angle(degrees: 60), angle * 2) + XCTAssertEqual(Angle(degrees: 60), 2 * angle) + XCTAssertEqual(Angle(degrees: 15), angle / 2) + angle += Angle(degrees: 10) + XCTAssertEqual(Angle(degrees: 40), angle) + angle -= Angle(degrees: 20) + XCTAssertEqual(Angle(degrees: 20), angle) + angle *= 3 + XCTAssertEqual(Angle(degrees: 60), angle) + angle /= 6 + XCTAssertEqual(Angle(degrees: 10), angle) } } @@ -114,6 +130,7 @@ final class AngleTests: XCTestCase { Float16.conversionBetweenRadiansAndDegreesChecks() Float16.trigonometricFunctionChecks() Float16.specialDegreesTrigonometricFunctionChecks() + Float16.additiveArithmeticTests() } } #endif @@ -122,12 +139,14 @@ final class AngleTests: XCTestCase { Float.conversionBetweenRadiansAndDegreesChecks() Float.trigonometricFunctionChecks() Float.specialDegreesTrigonometricFunctionChecks() + Float.additiveArithmeticTests() } func testDouble() { Double.conversionBetweenRadiansAndDegreesChecks() Double.trigonometricFunctionChecks() Double.specialDegreesTrigonometricFunctionChecks() + Double.additiveArithmeticTests() } #if (arch(i386) || arch(x86_64)) && !os(Windows) && !os(Android) @@ -135,6 +154,7 @@ final class AngleTests: XCTestCase { Float80.conversionBetweenRadiansAndDegreesChecks() Float80.trigonometricFunctionChecks() Float80.specialDegreesTrigonometricFunctionChecks() + Float80.additiveArithmeticTests() } #endif } From e09ef7a7fffa8c9d3450b3acc4862cbe9e940b2e Mon Sep 17 00:00:00 2001 From: jkaliak Date: Mon, 14 Dec 2020 01:11:10 +0100 Subject: [PATCH 14/22] Fixed conformance to Equatable, and added range containment functions with tests --- Sources/RealModule/Angle.swift | 31 +++++++++++++++++++++++++++---- Tests/RealTests/AngleTests.swift | 20 ++++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index 7ebe3e60..cf17db93 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -14,7 +14,7 @@ /// /// All trigonometric functions expect the argument to be passed as radians (Real), but this is not enforced by the type system. /// This type serves exactly this purpose, and can be seen as an alternative to the underlying Real implementation. -public struct Angle { +public struct Angle: Equatable { public var radians: T public init(radians: T) { self.radians = radians } public static func radians(_ val: T) -> Angle { .init(radians: val) } @@ -160,9 +160,32 @@ public extension Angle { } } -extension Angle: Equatable { - public static func == (lhs: Angle, rhs: Angle) -> Bool { - lhs.radians.isApproximatelyEqual(to: rhs.radians) +public extension Angle { + /// Checks whether the current angle is contained within a given closed range. + /// + /// - Parameters: + /// + /// - range: The closed angular range within which containment is checked. + func contained(in range: ClosedRange>) -> Bool { + range.contains(self) + } + + /// Checks whether the current angle is contained within a given half-open range. + /// + /// - Parameters: + /// + /// - range: The half-open angular range within which containment is checked. + func contained(in range: Range>) -> Bool { + range.contains(self) + } +} + +extension Angle: Comparable { + public static func < (lhs: Angle, rhs: Angle) -> Bool { + guard lhs != rhs else { + return false + } + return lhs.radians < rhs.radians } } diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index ddacd7c4..98aef8e1 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -107,8 +107,8 @@ where Self: BinaryFloatingPoint { static func additiveArithmeticTests() { var angle = Angle(degrees: 30) - XCTAssertEqual(Angle(degrees: 50), angle + Angle(degrees: 20)) - XCTAssertEqual(Angle(degrees: 10), angle - Angle(degrees: 20)) + assertClose(50, (angle + Angle(degrees: 20)).degrees) + assertClose(10, (angle - Angle(degrees: 20)).degrees) XCTAssertEqual(Angle(degrees: 60), angle * 2) XCTAssertEqual(Angle(degrees: 60), 2 * angle) XCTAssertEqual(Angle(degrees: 15), angle / 2) @@ -121,6 +121,18 @@ where Self: BinaryFloatingPoint { angle /= 6 XCTAssertEqual(Angle(degrees: 10), angle) } + + static func rangeContainmentTests() { + let angle = Angle(degrees: 30) + XCTAssertTrue(angle.contained(in: Angle(degrees: 10)...Angle(degrees: 40))) + XCTAssertTrue(angle.contained(in: Angle(degrees: 10)...Angle(degrees: 30))) + XCTAssertTrue(angle.contained(in: Angle(degrees: 30)...Angle(degrees: 40))) + XCTAssertFalse(angle.contained(in: Angle(degrees: 10)...Angle(degrees: 20))) + XCTAssertFalse(angle.contained(in: Angle(degrees: 50)...Angle(degrees: 60))) + XCTAssertTrue(angle.contained(in: Angle(degrees: 30).. Date: Mon, 14 Dec 2020 01:26:39 +0100 Subject: [PATCH 15/22] Updated public access modifier style --- Sources/RealModule/Angle.swift | 36 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index cf17db93..eca32abb 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -27,12 +27,12 @@ public struct Angle: Equatable { public static func degrees(_ val: T) -> Angle { .init(degrees: val) } } -public extension ElementaryFunctions +extension ElementaryFunctions where Self: Real { /// See also: /// - /// `ElementaryFunctions.cos()` - static func cos(_ angle: Angle) -> Self { + public static func cos(_ angle: Angle) -> Self { let normalizedRadians = normalize(angle.radians, limit: .pi) if -.pi/4 < normalizedRadians && normalizedRadians < .pi/4 { @@ -53,7 +53,7 @@ where Self: Real { /// See also: /// - /// `ElementaryFunctions.sin()` - static func sin(_ angle: Angle) -> Self { + public static func sin(_ angle: Angle) -> Self { let normalizedRadians = normalize(angle.radians, limit: .pi) if .pi / 4 < normalizedRadians && normalizedRadians < 3 * .pi / 4 { @@ -78,7 +78,7 @@ where Self: Real { /// See also: /// - /// `ElementaryFunctions.tan()` - static func tan(_ angle: Angle) -> Self { + public static func tan(_ angle: Angle) -> Self { let sine = sin(angle) let cosine = cos(angle) @@ -94,26 +94,26 @@ where Self: Real { } } -public extension Angle { +extension Angle { /// See also: /// - /// `ElementaryFunctions.acos()` - static func acos(_ x: T) -> Self { Angle.radians(T.acos(x)) } + public static func acos(_ x: T) -> Self { Angle.radians(T.acos(x)) } /// See also: /// - /// `ElementaryFunctions.asin()` - static func asin(_ x: T) -> Self { Angle.radians(T.asin(x)) } + public static func asin(_ x: T) -> Self { Angle.radians(T.asin(x)) } /// See also: /// - /// `ElementaryFunctions.atan()` - static func atan(_ x: T) -> Self { Angle.radians(T.atan(x)) } + public static func atan(_ x: T) -> Self { Angle.radians(T.atan(x)) } /// See also: /// - /// `RealFunctions.atan2()` - static func atan2(y: T, x: T) -> Self { Angle.radians(T.atan2(y: y, x: x)) } + public static func atan2(y: T, x: T) -> Self { Angle.radians(T.atan2(y: y, x: x)) } } extension Angle: AdditiveArithmetic { @@ -136,37 +136,37 @@ extension Angle: AdditiveArithmetic { } } -public extension Angle { - static func * (lhs: Angle, rhs: T) -> Angle { +extension Angle { + public static func * (lhs: Angle, rhs: T) -> Angle { Angle(radians: lhs.radians * rhs) } - static func *= (lhs: inout Angle, rhs: T) { + public static func *= (lhs: inout Angle, rhs: T) { lhs.radians *= rhs } - static func * (lhs: T, rhs: Angle) -> Angle { + public static func * (lhs: T, rhs: Angle) -> Angle { Angle(radians: rhs.radians * lhs) } - static func / (lhs: Angle, rhs: T) -> Angle { + public static func / (lhs: Angle, rhs: T) -> Angle { assert(rhs != 0) return Angle(radians: lhs.radians / rhs) } - static func /= (lhs: inout Angle, rhs: T) { + public static func /= (lhs: inout Angle, rhs: T) { assert(rhs != 0) lhs.radians /= rhs } } -public extension Angle { +extension Angle { /// Checks whether the current angle is contained within a given closed range. /// /// - Parameters: /// /// - range: The closed angular range within which containment is checked. - func contained(in range: ClosedRange>) -> Bool { + public func contained(in range: ClosedRange>) -> Bool { range.contains(self) } @@ -175,7 +175,7 @@ public extension Angle { /// - Parameters: /// /// - range: The half-open angular range within which containment is checked. - func contained(in range: Range>) -> Bool { + public func contained(in range: Range>) -> Bool { range.contains(self) } } From 949c9a200921d59c74738abeda6e04d61572bcb0 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Wed, 16 Dec 2020 22:24:07 +0100 Subject: [PATCH 16/22] Refactored AngleTests --- Tests/RealTests/AngleTests.swift | 33 +++++++++++++------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 98aef8e1..6c07d345 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -136,41 +136,34 @@ where Self: BinaryFloatingPoint { } final class AngleTests: XCTestCase { + private func execute(`for` Type: T.Type) { + Type.conversionBetweenRadiansAndDegreesChecks() + Type.trigonometricFunctionChecks() + Type.specialDegreesTrigonometricFunctionChecks() + Type.additiveArithmeticTests() + Type.rangeContainmentTests() + } + + #if swift(>=5.3) && !(os(macOS) || os(iOS) && targetEnvironment(macCatalyst)) func testFloat16() { if #available(iOS 14.0, watchOS 14.0, tvOS 7.0, *) { - Float16.conversionBetweenRadiansAndDegreesChecks() - Float16.trigonometricFunctionChecks() - Float16.specialDegreesTrigonometricFunctionChecks() - Float16.additiveArithmeticTests() - Float16.rangeContainmentTests() + execute(for: Float16.self) } } #endif func testFloat() { - Float.conversionBetweenRadiansAndDegreesChecks() - Float.trigonometricFunctionChecks() - Float.specialDegreesTrigonometricFunctionChecks() - Float.additiveArithmeticTests() - Float.rangeContainmentTests() + execute(for: Float.self) } func testDouble() { - Double.conversionBetweenRadiansAndDegreesChecks() - Double.trigonometricFunctionChecks() - Double.specialDegreesTrigonometricFunctionChecks() - Double.additiveArithmeticTests() - Double.rangeContainmentTests() + execute(for: Double.self) } #if (arch(i386) || arch(x86_64)) && !os(Windows) && !os(Android) func testFloat80() { - Float80.conversionBetweenRadiansAndDegreesChecks() - Float80.trigonometricFunctionChecks() - Float80.specialDegreesTrigonometricFunctionChecks() - Float80.additiveArithmeticTests() - Float80.rangeContainmentTests() + execute(for: Float80.self) } #endif } From 63bdaaf64a2b00fee4c350269fdb602a8ebeed18 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Fri, 18 Dec 2020 23:36:16 +0100 Subject: [PATCH 17/22] cos and sin for exact degrees --- Sources/RealModule/Angle.swift | 390 +++++++++++++++++++++---------- Tests/RealTests/AngleTests.swift | 103 ++++---- 2 files changed, 316 insertions(+), 177 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index eca32abb..ee153102 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -15,191 +15,323 @@ /// All trigonometric functions expect the argument to be passed as radians (Real), but this is not enforced by the type system. /// This type serves exactly this purpose, and can be seen as an alternative to the underlying Real implementation. public struct Angle: Equatable { - public var radians: T - public init(radians: T) { self.radians = radians } - public static func radians(_ val: T) -> Angle { .init(radians: val) } + fileprivate init() {} - public var degrees: T { radians * 180 / .pi } - public init(degrees: T) { - let normalized = normalize(degrees, limit: 180) - self.init(radians: normalized * .pi / 180) + fileprivate init(degreesPart: T, radiansPart: T) { + self.degreesPart = degreesPart + self.radiansPart = radiansPart } - public static func degrees(_ val: T) -> Angle { .init(degrees: val) } -} - -extension ElementaryFunctions -where Self: Real { - /// See also: - /// - - /// `ElementaryFunctions.cos()` - public static func cos(_ angle: Angle) -> Self { - let normalizedRadians = normalize(angle.radians, limit: .pi) - - if -.pi/4 < normalizedRadians && normalizedRadians < .pi/4 { - return Self.cos(normalizedRadians) - } - - if normalizedRadians > 3 * .pi / 4 || normalizedRadians < -3 * .pi / 4 { - return -Self.cos(.pi - normalizedRadians) - } - - if normalizedRadians >= 0 { - return Self.sin(.pi/2 - normalizedRadians) - } - - return Self.sin(normalizedRadians + .pi / 2) + + public init(radians: T) { + radiansPart = radians } - /// See also: - /// - - /// `ElementaryFunctions.sin()` - public static func sin(_ angle: Angle) -> Self { - let normalizedRadians = normalize(angle.radians, limit: .pi) - - if .pi / 4 < normalizedRadians && normalizedRadians < 3 * .pi / 4 { - return Self.sin(normalizedRadians) - } - - if -3 * .pi / 4 < normalizedRadians && normalizedRadians < -.pi / 4 { - return -Self.sin(-normalizedRadians) - } - - if normalizedRadians > 3 * .pi / 4 { - return Self.sin(.pi - normalizedRadians) - } - - if normalizedRadians < -3 * .pi / 4 { - return -Self.sin(.pi + normalizedRadians) - } - - return Self.sin(normalizedRadians) + public static func radians(_ value: T) -> Angle { + .init(radians: value) } - /// See also: - /// - - /// `ElementaryFunctions.tan()` - public static func tan(_ angle: Angle) -> Self { - let sine = sin(angle) - let cosine = cos(angle) - - guard cosine != 0 else { - var result = Self.infinity - if sine.sign == .minus { - result.negate() - } - return result - } - - return sine / cosine + public init(degrees: T) { + degreesPart = degrees } -} - -extension Angle { - /// See also: - /// - - /// `ElementaryFunctions.acos()` - public static func acos(_ x: T) -> Self { Angle.radians(T.acos(x)) } - /// See also: - /// - - /// `ElementaryFunctions.asin()` - public static func asin(_ x: T) -> Self { Angle.radians(T.asin(x)) } + public static func degrees(_ value: T) -> Angle { + .init(degrees: value) + } - /// See also: - /// - - /// `ElementaryFunctions.atan()` - public static func atan(_ x: T) -> Self { Angle.radians(T.atan(x)) } + public var radians: T { + radiansPart + degreesPart.asRadians + } - /// See also: - /// - - /// `RealFunctions.atan2()` - public static func atan2(y: T, x: T) -> Self { Angle.radians(T.atan2(y: y, x: x)) } + public var degrees: T { + radiansPart.asDegrees + degreesPart + } + + fileprivate var degreesPart: T = 0 + + fileprivate var radiansPart: T = 0 } extension Angle: AdditiveArithmetic { - public static var zero: Angle { .radians(0) } + public static var zero: Angle { .init() } public static func + (lhs: Angle, rhs: Angle) -> Angle { - Angle(radians: lhs.radians + rhs.radians) + .init(degreesPart: lhs.degreesPart + rhs.degreesPart, + radiansPart: lhs.radiansPart + rhs.radiansPart) } public static func += (lhs: inout Angle, rhs: Angle) { - lhs.radians += rhs.radians + lhs.degreesPart += rhs.degreesPart + lhs.radiansPart += rhs.radiansPart } public static func - (lhs: Angle, rhs: Angle) -> Angle { - Angle(radians: lhs.radians - rhs.radians) + .init(degreesPart: lhs.degreesPart - rhs.degreesPart, + radiansPart: lhs.radiansPart - rhs.radiansPart) } public static func -= (lhs: inout Angle, rhs: Angle) { - lhs.radians -= rhs.radians + lhs.degreesPart -= rhs.degreesPart + lhs.radiansPart -= rhs.radiansPart } } extension Angle { public static func * (lhs: Angle, rhs: T) -> Angle { - Angle(radians: lhs.radians * rhs) + .init(degreesPart: lhs.degreesPart * rhs, + radiansPart: lhs.radiansPart * rhs) } public static func *= (lhs: inout Angle, rhs: T) { - lhs.radians *= rhs + lhs.degreesPart *= rhs + lhs.radiansPart *= rhs } public static func * (lhs: T, rhs: Angle) -> Angle { - Angle(radians: rhs.radians * lhs) + return rhs * lhs } public static func / (lhs: Angle, rhs: T) -> Angle { assert(rhs != 0) - return Angle(radians: lhs.radians / rhs) + return .init(degreesPart: lhs.degreesPart / rhs, + radiansPart: lhs.radiansPart / rhs) } public static func /= (lhs: inout Angle, rhs: T) { assert(rhs != 0) - lhs.radians /= rhs + lhs.degreesPart /= rhs + lhs.radiansPart /= rhs } } -extension Angle { - /// Checks whether the current angle is contained within a given closed range. - /// - /// - Parameters: - /// - /// - range: The closed angular range within which containment is checked. - public func contained(in range: ClosedRange>) -> Bool { - range.contains(self) - } - - /// Checks whether the current angle is contained within a given half-open range. - /// - /// - Parameters: - /// - /// - range: The half-open angular range within which containment is checked. - public func contained(in range: Range>) -> Bool { - range.contains(self) +extension ElementaryFunctions +where Self: Real { + /// See also: + /// - + /// `ElementaryFunctions.cos()` + public static func cos(_ angle: Angle) -> Self { + let degrees = angle.normalizedDegrees() + let cosa = cosd(degrees) + let cosb = cos(angle.radiansPart) + let sina = sind(degrees) + let sinb = sin(angle.radiansPart) + return cosa * cosb - sina * sinb + } + + private static func cosd(_ degrees: Self) -> Self { + let (exactPart, rest) = degrees.extractParts() + guard let knownAngle = exactPart else { + return cos(rest.asRadians) + } + let (cosa, sina) = getKnownTrigonometry(for: knownAngle) + let cosb = cosd(rest) + let sinb = sind(rest) + return cosa * cosb - sina * sinb } } -extension Angle: Comparable { - public static func < (lhs: Angle, rhs: Angle) -> Bool { - guard lhs != rhs else { - return false +extension ElementaryFunctions +where Self: Real { + /// See also: + /// - + /// `ElementaryFunctions.sin()` + public static func sin(_ angle: Angle) -> Self { + let degrees = angle.normalizedDegrees() + let cosa = cosd(degrees) + let cosb = cos(angle.radiansPart) + let sina = sind(degrees) + let sinb = sin(angle.radiansPart) + return sina * cosb + cosa * sinb + } + + private static func sind(_ degrees: Self) -> Self { + let (exactPart, rest) = degrees.extractParts() + guard let knownAngle = exactPart else { + return sin(rest.asRadians) } - return lhs.radians < rhs.radians + let (cosa, sina) = getKnownTrigonometry(for: knownAngle) + let cosb = cosd(rest) + let sinb = sind(rest) + return sina * cosb + cosa * sinb + } + + // + // /// See also: + // /// - + // /// `ElementaryFunctions.tan()` + // public static func tan(_ angle: Angle) -> Self { + // let sine = sin(angle) + // let cosine = cos(angle) + // + // guard cosine != 0 else { + // var result = Self.infinity + // if sine.sign == .minus { + // result.negate() + // } + // return result + // } + // + // return sine / cosine + // } +} + +extension ElementaryFunctions +where Self: Real { + fileprivate static func getKnownTrigonometry(`for` degrees: Self) -> (cos: Self, sin: Self) { + let knownTrigonometry = commonAngleConversions().first(where: { $0.degrees == degrees.magnitude })! + return (knownTrigonometry.cos, knownTrigonometry.sin * degrees.realSign) } } -private func normalize(_ input: T, limit: T) -> T -where T: Real { - var normalized = input +//extension Angle { +// /// See also: +// /// - +// /// `ElementaryFunctions.acos()` +// public static func acos(_ x: T) -> Self { Angle.radians(T.acos(x)) } +// +// /// See also: +// /// - +// /// `ElementaryFunctions.asin()` +// public static func asin(_ x: T) -> Self { Angle.radians(T.asin(x)) } +// +// /// See also: +// /// - +// /// `ElementaryFunctions.atan()` +// public static func atan(_ x: T) -> Self { Angle.radians(T.atan(x)) } +// +// /// See also: +// /// - +// /// `RealFunctions.atan2()` +// public static func atan2(y: T, x: T) -> Self { Angle.radians(T.atan2(y: y, x: x)) } +//} +// +// +//extension Angle { +// /// Checks whether the current angle is contained within a given closed range. +// /// +// /// - Parameters: +// /// +// /// - range: The closed angular range within which containment is checked. +// public func contained(in range: ClosedRange>) -> Bool { +// range.contains(self) +// } +// +// /// Checks whether the current angle is contained within a given half-open range. +// /// +// /// - Parameters: +// /// +// /// - range: The half-open angular range within which containment is checked. +// public func contained(in range: Range>) -> Bool { +// range.contains(self) +// } +//} +// +//extension Angle { +// // “Is angle δ no more than angle ε away from angle ζ?” +//} +// +//extension Angle: Comparable { +// public static func < (lhs: Angle, rhs: Angle) -> Bool { +// guard lhs != rhs else { +// return false +// } +// return lhs.radians < rhs.radians +// } +//} +// + +extension Real { + fileprivate var asRadians: Self { self * .pi / 180 } + + fileprivate var asDegrees: Self { self * 180 / .pi } + + fileprivate func extractParts() -> (common: Self?, rest: Self) { + if let summandsAbove90 = extractParts(limit: 90) { + return (summandsAbove90.common, summandsAbove90.rest) + } + if let summandsAbove60 = extractParts(limit: 60) { + return (summandsAbove60.common, summandsAbove60.rest) + } + if let summandsAbove45 = extractParts(limit: 45) { + return (summandsAbove45.common, summandsAbove45.rest) + } + if let summandsAbove30 = extractParts(limit: 30) { + return (summandsAbove30.common, summandsAbove30.rest) + } + return (nil, self) + } - while normalized > limit { - normalized -= 2 * limit + private func extractParts(limit: Self) -> DegreesSummands? { + guard self.magnitude >= limit else { + return nil + } + return DegreesSummands(common: realSign * limit, + rest: realSign * (self.magnitude - limit)) + } +} + +private struct DegreesSummands { + let common: T + let rest: T +} + +extension Angle { + fileprivate func normalizedDegrees() -> T { + normalize(\.degreesPart, limit: 180) } - while normalized < -limit { - normalized += 2 * limit + fileprivate func normalizedRadians() -> T { + normalize(\.radiansPart, limit: .pi) } - return normalized + private func normalize(_ path: KeyPath, limit: T) -> T { + var normalized = self[keyPath: path] + + while normalized > limit { + normalized -= 2 * limit + } + + while normalized < -limit { + normalized += 2 * limit + } + + return normalized + } +} + +private struct AccurateTrigonometry { + let degrees: T + let cos: T + let sin: T + let tan: T +} + +private func commonAngleConversions() -> [AccurateTrigonometry] { + [ + AccurateTrigonometry(degrees: 0, + cos: 1, + sin: 0, + tan: 0), + AccurateTrigonometry(degrees: 30, + cos: T.sqrt(3)/2, + sin: 1 / 2, + tan: T.sqrt(3) / 3), + AccurateTrigonometry(degrees: 45, + cos: T.sqrt(2) / 2, + sin: T.sqrt(2) / 2, + tan: 1), + AccurateTrigonometry(degrees: 60, + cos: 1 / 2, + sin: T.sqrt(3) / 2, + tan: T.sqrt(3)), + AccurateTrigonometry(degrees: 90, + cos: 0, + sin: 1, + tan: T.infinity), + ] +} + +extension Real { + fileprivate var realSign: Self { + self > 0 ? 1 : -1 + } } diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 6c07d345..3185512f 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -26,17 +26,17 @@ where Self: BinaryFloatingPoint { assertClose(2 * Double(Self.pi) / 3, angleFromDegrees.radians) } - static func trigonometricFunctionChecks() { - assertClose(1.1863995522992575361931268186727044683, Angle.acos(0.375).radians) - assertClose(0.3843967744956390830381948729670469737, Angle.asin(0.375).radians) - assertClose(0.3587706702705722203959200639264604997, Angle.atan(0.375).radians) - assertClose(0.54041950027058415544357836460859991, Angle.atan2(y: 0.375, x: 0.625).radians) - - assertClose(0.9305076219123142911494767922295555080, cos(Angle(radians: 0.375))) - assertClose(0.3662725290860475613729093517162641571, sin(Angle(radians: 0.375))) - assertClose(0.3936265759256327582294137871012180981, tan(Angle(radians: 0.375))) - } - +// static func trigonometricFunctionChecks() { +// assertClose(1.1863995522992575361931268186727044683, Angle.acos(0.375).radians) +// assertClose(0.3843967744956390830381948729670469737, Angle.asin(0.375).radians) +// assertClose(0.3587706702705722203959200639264604997, Angle.atan(0.375).radians) +// assertClose(0.54041950027058415544357836460859991, Angle.atan2(y: 0.375, x: 0.625).radians) +// +// assertClose(0.9305076219123142911494767922295555080, cos(Angle(radians: 0.375))) +// assertClose(0.3662725290860475613729093517162641571, sin(Angle(radians: 0.375))) +// assertClose(0.3936265759256327582294137871012180981, tan(Angle(radians: 0.375))) +// } +// static func specialDegreesTrigonometricFunctionChecks() { XCTAssertEqual(1, cos(Angle(degrees: -360))) XCTAssertEqual(0, cos(Angle(degrees: -270))) @@ -59,7 +59,7 @@ where Self: BinaryFloatingPoint { XCTAssertEqual(-1, cos(Angle(degrees: 180))) XCTAssertEqual(0, cos(Angle(degrees: 270))) XCTAssertEqual(1, cos(Angle(degrees: 360))) - + XCTAssertEqual(0, sin(Angle(degrees: -360))) XCTAssertEqual(1, sin(Angle(degrees: -270))) XCTAssertEqual(0, sin(Angle(degrees: -180))) @@ -82,30 +82,37 @@ where Self: BinaryFloatingPoint { XCTAssertEqual(-1, sin(Angle(degrees: 270))) XCTAssertEqual(0, sin(Angle(degrees: 360))) - XCTAssertEqual(0, tan(Angle(degrees: -360))) - XCTAssertEqual(.infinity, tan(Angle(degrees: -270))) - XCTAssertEqual(0, tan(Angle(degrees: -180))) - assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: -150))) - XCTAssertEqual(1, tan(Angle(degrees: -135))) - assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: -120))) - XCTAssertEqual(-.infinity, tan(Angle(degrees: -90))) - assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: -60))) - XCTAssertEqual(-1, tan(Angle(degrees: -45))) - assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: -30))) - XCTAssertEqual(0, tan(Angle(degrees: 0))) - assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) - XCTAssertEqual(1, tan(Angle(degrees: 45))) - assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: 60))) - XCTAssertEqual(.infinity, tan(Angle(degrees: 90))) - assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: 120))) - XCTAssertEqual(-1, tan(Angle(degrees: 135))) - assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: 150))) - XCTAssertEqual(0, tan(Angle(degrees: 180))) - XCTAssertEqual(-.infinity, tan(Angle(degrees: 270))) - XCTAssertEqual(0, tan(Angle(degrees: 360))) +// XCTAssertEqual(0, tan(Angle(degrees: -360))) +// XCTAssertEqual(.infinity, tan(Angle(degrees: -270))) +// XCTAssertEqual(0, tan(Angle(degrees: -180))) +// assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: -150))) +// XCTAssertEqual(1, tan(Angle(degrees: -135))) +// assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: -120))) +// XCTAssertEqual(-.infinity, tan(Angle(degrees: -90))) +// assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: -60))) +// XCTAssertEqual(-1, tan(Angle(degrees: -45))) +// assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: -30))) +// XCTAssertEqual(0, tan(Angle(degrees: 0))) +// assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) +// XCTAssertEqual(1, tan(Angle(degrees: 45))) +// assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: 60))) +// XCTAssertEqual(.infinity, tan(Angle(degrees: 90))) +// assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: 120))) +// XCTAssertEqual(-1, tan(Angle(degrees: 135))) +// assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: 150))) +// XCTAssertEqual(0, tan(Angle(degrees: 180))) +// XCTAssertEqual(-.infinity, tan(Angle(degrees: 270))) +// XCTAssertEqual(0, tan(Angle(degrees: 360))) } - + static func additiveArithmeticTests() { + let angle1 = Angle(degrees: 90) + let angle2 = Angle(radians: .pi) + let sum = angle1 + angle2 + XCTAssertEqual(270, sum.degrees) + XCTAssertEqual(3 * .pi / 2, sum.radians) + XCTAssertEqual(360, (sum + angle1).degrees) + XCTAssertEqual(2 * .pi, (sum + angle1).radians) var angle = Angle(degrees: 30) assertClose(50, (angle + Angle(degrees: 20)).degrees) assertClose(10, (angle - Angle(degrees: 20)).degrees) @@ -121,27 +128,27 @@ where Self: BinaryFloatingPoint { angle /= 6 XCTAssertEqual(Angle(degrees: 10), angle) } - - static func rangeContainmentTests() { - let angle = Angle(degrees: 30) - XCTAssertTrue(angle.contained(in: Angle(degrees: 10)...Angle(degrees: 40))) - XCTAssertTrue(angle.contained(in: Angle(degrees: 10)...Angle(degrees: 30))) - XCTAssertTrue(angle.contained(in: Angle(degrees: 30)...Angle(degrees: 40))) - XCTAssertFalse(angle.contained(in: Angle(degrees: 10)...Angle(degrees: 20))) - XCTAssertFalse(angle.contained(in: Angle(degrees: 50)...Angle(degrees: 60))) - XCTAssertTrue(angle.contained(in: Angle(degrees: 30)..(`for` Type: T.Type) { Type.conversionBetweenRadiansAndDegreesChecks() - Type.trigonometricFunctionChecks() +// Type.trigonometricFunctionChecks() Type.specialDegreesTrigonometricFunctionChecks() Type.additiveArithmeticTests() - Type.rangeContainmentTests() +// Type.rangeContainmentTests() } From acd67fbc50ddbe890b8c0e67991babf1a0863fef Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sat, 19 Dec 2020 00:14:17 +0100 Subject: [PATCH 18/22] exact tan implemented --- Sources/RealModule/Angle.swift | 182 ++++++++++++++++++------------- Tests/RealTests/AngleTests.swift | 42 +++---- 2 files changed, 127 insertions(+), 97 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index ee153102..72be5ede 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -109,22 +109,26 @@ where Self: Real { /// - /// `ElementaryFunctions.cos()` public static func cos(_ angle: Angle) -> Self { - let degrees = angle.normalizedDegrees() + let degrees = angle.normalizedDegreesPart() let cosa = cosd(degrees) let cosb = cos(angle.radiansPart) let sina = sind(degrees) let sinb = sin(angle.radiansPart) - return cosa * cosb - sina * sinb + return cossum(cosa, cosb, sina, sinb) } private static func cosd(_ degrees: Self) -> Self { let (exactPart, rest) = degrees.extractParts() - guard let knownAngle = exactPart else { + guard let exactAngle = exactPart else { return cos(rest.asRadians) } - let (cosa, sina) = getKnownTrigonometry(for: knownAngle) + let (cosa, sina) = getExactCosAndSin(for: exactAngle) let cosb = cosd(rest) let sinb = sind(rest) + return cossum(cosa, cosb, sina, sinb) + } + + private static func cossum(_ cosa: Self, _ cosb: Self, _ sina: Self, _ sinb: Self) -> Self { return cosa * cosb - sina * sinb } } @@ -135,75 +139,99 @@ where Self: Real { /// - /// `ElementaryFunctions.sin()` public static func sin(_ angle: Angle) -> Self { - let degrees = angle.normalizedDegrees() + let degrees = angle.normalizedDegreesPart() let cosa = cosd(degrees) let cosb = cos(angle.radiansPart) let sina = sind(degrees) let sinb = sin(angle.radiansPart) - return sina * cosb + cosa * sinb + return sinsum(cosa, cosb, sina, sinb) } private static func sind(_ degrees: Self) -> Self { let (exactPart, rest) = degrees.extractParts() - guard let knownAngle = exactPart else { + guard let exactAngle = exactPart else { return sin(rest.asRadians) } - let (cosa, sina) = getKnownTrigonometry(for: knownAngle) + let (cosa, sina) = getExactCosAndSin(for: exactAngle) let cosb = cosd(rest) let sinb = sind(rest) + return sinsum(cosa, cosb, sina, sinb) + } + + private static func sinsum(_ cosa: Self, _ cosb: Self, _ sina: Self, _ sinb: Self) -> Self { return sina * cosb + cosa * sinb } +} + +extension ElementaryFunctions +where Self: Real { + /// See also: + /// - + /// `ElementaryFunctions.tan()` + public static func tan(_ angle: Angle) -> Self { + let degrees = angle.normalizedDegreesPart() + let tana = tand(degrees) + let tanb = tan(angle.radiansPart) + return tansum(tana, tanb) + } - // - // /// See also: - // /// - - // /// `ElementaryFunctions.tan()` - // public static func tan(_ angle: Angle) -> Self { - // let sine = sin(angle) - // let cosine = cos(angle) - // - // guard cosine != 0 else { - // var result = Self.infinity - // if sine.sign == .minus { - // result.negate() - // } - // return result - // } - // - // return sine / cosine - // } + private static func tand(_ degrees: Self) -> Self { + let (exactPart, rest) = degrees.extractParts() + guard let exactAngle = exactPart else { + return tan(rest.asRadians) + } + let tana = getExactTan(for: exactAngle) + let tanb = tand(rest) + return tansum(tana, tanb) + } + + private static func tansum(_ tana: Self, _ tanb: Self) -> Self { + switch (tana.isFinite, tanb) { + case (false, 0): + return tana + case (false, _): + return -1 / tanb + default: + return (tana + tanb) / (1 - tana * tanb) + } + } } extension ElementaryFunctions where Self: Real { - fileprivate static func getKnownTrigonometry(`for` degrees: Self) -> (cos: Self, sin: Self) { - let knownTrigonometry = commonAngleConversions().first(where: { $0.degrees == degrees.magnitude })! + fileprivate static func getExactCosAndSin(for degrees: Self) -> (cos: Self, sin: Self) { + let knownTrigonometry = exactAngleConversions().first(where: { $0.degrees == degrees.magnitude })! return (knownTrigonometry.cos, knownTrigonometry.sin * degrees.realSign) } + + fileprivate static func getExactTan(for degrees: Self) -> Self { + let knownTrigonometry = exactAngleConversions().first(where: { $0.degrees == degrees.magnitude })! + return knownTrigonometry.tan * degrees.realSign + } +} + +extension Angle { + /// See also: + /// - + /// `ElementaryFunctions.acos()` + public static func acos(_ x: T) -> Self { Angle.radians(T.acos(x)) } + + /// See also: + /// - + /// `ElementaryFunctions.asin()` + public static func asin(_ x: T) -> Self { Angle.radians(T.asin(x)) } + + /// See also: + /// - + /// `ElementaryFunctions.atan()` + public static func atan(_ x: T) -> Self { Angle.radians(T.atan(x)) } + + /// See also: + /// - + /// `RealFunctions.atan2()` + public static func atan2(y: T, x: T) -> Self { Angle.radians(T.atan2(y: y, x: x)) } } -//extension Angle { -// /// See also: -// /// - -// /// `ElementaryFunctions.acos()` -// public static func acos(_ x: T) -> Self { Angle.radians(T.acos(x)) } -// -// /// See also: -// /// - -// /// `ElementaryFunctions.asin()` -// public static func asin(_ x: T) -> Self { Angle.radians(T.asin(x)) } -// -// /// See also: -// /// - -// /// `ElementaryFunctions.atan()` -// public static func atan(_ x: T) -> Self { Angle.radians(T.atan(x)) } -// -// /// See also: -// /// - -// /// `RealFunctions.atan2()` -// public static func atan2(y: T, x: T) -> Self { Angle.radians(T.atan2(y: y, x: x)) } -//} -// // //extension Angle { // /// Checks whether the current angle is contained within a given closed range. @@ -228,16 +256,15 @@ where Self: Real { //extension Angle { // // “Is angle δ no more than angle ε away from angle ζ?” //} -// -//extension Angle: Comparable { -// public static func < (lhs: Angle, rhs: Angle) -> Bool { -// guard lhs != rhs else { -// return false -// } -// return lhs.radians < rhs.radians -// } -//} -// + +extension Angle: Comparable { + public static func < (lhs: Angle, rhs: Angle) -> Bool { + guard lhs != rhs else { + return false + } + return lhs.radians < rhs.radians + } +} extension Real { fileprivate var asRadians: Self { self * .pi / 180 } @@ -246,16 +273,16 @@ extension Real { fileprivate func extractParts() -> (common: Self?, rest: Self) { if let summandsAbove90 = extractParts(limit: 90) { - return (summandsAbove90.common, summandsAbove90.rest) + return (summandsAbove90.exact, summandsAbove90.rest) } if let summandsAbove60 = extractParts(limit: 60) { - return (summandsAbove60.common, summandsAbove60.rest) + return (summandsAbove60.exact, summandsAbove60.rest) } if let summandsAbove45 = extractParts(limit: 45) { - return (summandsAbove45.common, summandsAbove45.rest) + return (summandsAbove45.exact, summandsAbove45.rest) } if let summandsAbove30 = extractParts(limit: 30) { - return (summandsAbove30.common, summandsAbove30.rest) + return (summandsAbove30.exact, summandsAbove30.rest) } return (nil, self) } @@ -264,22 +291,22 @@ extension Real { guard self.magnitude >= limit else { return nil } - return DegreesSummands(common: realSign * limit, + return DegreesSummands(exact: realSign * limit, rest: realSign * (self.magnitude - limit)) } } private struct DegreesSummands { - let common: T + let exact: T let rest: T } extension Angle { - fileprivate func normalizedDegrees() -> T { + fileprivate func normalizedDegreesPart() -> T { normalize(\.degreesPart, limit: 180) } - fileprivate func normalizedRadians() -> T { + fileprivate func normalizedRadiansPart() -> T { normalize(\.radiansPart, limit: .pi) } @@ -298,32 +325,32 @@ extension Angle { } } -private struct AccurateTrigonometry { +private struct ExactTrigonometry { let degrees: T let cos: T let sin: T let tan: T } -private func commonAngleConversions() -> [AccurateTrigonometry] { +private func exactAngleConversions() -> [ExactTrigonometry] { [ - AccurateTrigonometry(degrees: 0, + ExactTrigonometry(degrees: 0, cos: 1, sin: 0, tan: 0), - AccurateTrigonometry(degrees: 30, + ExactTrigonometry(degrees: 30, cos: T.sqrt(3)/2, sin: 1 / 2, tan: T.sqrt(3) / 3), - AccurateTrigonometry(degrees: 45, + ExactTrigonometry(degrees: 45, cos: T.sqrt(2) / 2, sin: T.sqrt(2) / 2, tan: 1), - AccurateTrigonometry(degrees: 60, + ExactTrigonometry(degrees: 60, cos: 1 / 2, sin: T.sqrt(3) / 2, tan: T.sqrt(3)), - AccurateTrigonometry(degrees: 90, + ExactTrigonometry(degrees: 90, cos: 0, sin: 1, tan: T.infinity), @@ -332,6 +359,9 @@ private func commonAngleConversions() -> [AccurateTrigonometry] { extension Real { fileprivate var realSign: Self { - self > 0 ? 1 : -1 + guard self.isFinite else { + return self == .infinity ? 1 : -1 + } + return self > 0 ? 1 : -1 } } diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 3185512f..db588320 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -82,27 +82,27 @@ where Self: BinaryFloatingPoint { XCTAssertEqual(-1, sin(Angle(degrees: 270))) XCTAssertEqual(0, sin(Angle(degrees: 360))) -// XCTAssertEqual(0, tan(Angle(degrees: -360))) -// XCTAssertEqual(.infinity, tan(Angle(degrees: -270))) -// XCTAssertEqual(0, tan(Angle(degrees: -180))) -// assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: -150))) -// XCTAssertEqual(1, tan(Angle(degrees: -135))) -// assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: -120))) -// XCTAssertEqual(-.infinity, tan(Angle(degrees: -90))) -// assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: -60))) -// XCTAssertEqual(-1, tan(Angle(degrees: -45))) -// assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: -30))) -// XCTAssertEqual(0, tan(Angle(degrees: 0))) -// assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) -// XCTAssertEqual(1, tan(Angle(degrees: 45))) -// assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: 60))) -// XCTAssertEqual(.infinity, tan(Angle(degrees: 90))) -// assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: 120))) -// XCTAssertEqual(-1, tan(Angle(degrees: 135))) -// assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: 150))) -// XCTAssertEqual(0, tan(Angle(degrees: 180))) -// XCTAssertEqual(-.infinity, tan(Angle(degrees: 270))) -// XCTAssertEqual(0, tan(Angle(degrees: 360))) + XCTAssertEqual(0, tan(Angle(degrees: -360))) + XCTAssertEqual(.infinity, tan(Angle(degrees: -270))) + XCTAssertEqual(0, tan(Angle(degrees: -180))) + assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: -150))) + XCTAssertEqual(1, tan(Angle(degrees: -135))) + assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: -120))) + XCTAssertEqual(-.infinity, tan(Angle(degrees: -90))) + assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: -60))) + XCTAssertEqual(-1, tan(Angle(degrees: -45))) + assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: -30))) + XCTAssertEqual(0, tan(Angle(degrees: 0))) + assertClose(0.57735026918962576450914878050195745565, tan(Angle(degrees: 30))) + XCTAssertEqual(1, tan(Angle(degrees: 45))) + assertClose(1.7320508075688772935274463415058723669, tan(Angle(degrees: 60))) + XCTAssertEqual(.infinity, tan(Angle(degrees: 90))) + assertClose(-1.7320508075688772935274463415058723669, tan(Angle(degrees: 120))) + XCTAssertEqual(-1, tan(Angle(degrees: 135))) + assertClose(-0.57735026918962576450914878050195745565, tan(Angle(degrees: 150))) + XCTAssertEqual(0, tan(Angle(degrees: 180))) + XCTAssertEqual(-.infinity, tan(Angle(degrees: 270))) + XCTAssertEqual(0, tan(Angle(degrees: 360))) } static func additiveArithmeticTests() { From 14c520b20e72be1a59d2a41ceaa9e0e39742ff81 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sat, 19 Dec 2020 23:22:43 +0100 Subject: [PATCH 19/22] inverse trigonometric functions fixed --- Sources/RealModule/Angle.swift | 45 +++++++++++++++++-- Tests/RealTests/AngleTests.swift | 75 ++++++++++++++++++++++++++------ 2 files changed, 102 insertions(+), 18 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index 72be5ede..1997ed31 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -214,22 +214,59 @@ extension Angle { /// See also: /// - /// `ElementaryFunctions.acos()` - public static func acos(_ x: T) -> Self { Angle.radians(T.acos(x)) } + public static func acos(_ x: T) -> Self { + guard let exactSolution = exactAngleConversions().first(where: {$0.cos == x.magnitude }) else { + return .radians(T.acos(x)) + } + let degrees = x > 0 + ? exactSolution.degrees + : 180 - exactSolution.degrees + return .degrees(degrees) + } /// See also: /// - /// `ElementaryFunctions.asin()` - public static func asin(_ x: T) -> Self { Angle.radians(T.asin(x)) } + public static func asin(_ x: T) -> Self { + guard let exactSolution = exactAngleConversions().first(where: { $0.sin == x.magnitude }) else { + return .radians(T.asin(x)) + } + return .degrees(exactSolution.degrees * x.realSign) + } /// See also: /// - /// `ElementaryFunctions.atan()` - public static func atan(_ x: T) -> Self { Angle.radians(T.atan(x)) } + public static func atan(_ x: T) -> Self { + guard let exactSolution = exactAngleConversions().first(where: { $0.tan == x.magnitude }) else { + return .radians(T.atan(x)) + } + return .degrees(exactSolution.degrees * x.realSign) + } /// See also: /// - /// `RealFunctions.atan2()` - public static func atan2(y: T, x: T) -> Self { Angle.radians(T.atan2(y: y, x: x)) } + public static func atan2(y: T, x: T) -> Self { + let norm = (x*x + y*y).squareRoot() + assert(norm != 0) + let sin = y / norm + let cos = x / norm + guard let exactSolution = exactAngleConversions().first(where: { cos.magnitude.isApproximatelyEqual(to: $0.cos) + && sin.magnitude.isApproximatelyEqual(to:$0.sin) }) else { + return .radians(T.atan2(y: y, x: x)) + } + if y >= 0 && x >= 0 { + return .degrees(exactSolution.degrees) + } + if y >= 0 && x < 0 { + return .degrees(180 - exactSolution.degrees) + } + if y < 0 && x >= 0 { + return .degrees(-exactSolution.degrees) + } + return .degrees(-180 + exactSolution.degrees) + } } // diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index db588320..7834dba3 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -26,18 +26,61 @@ where Self: BinaryFloatingPoint { assertClose(2 * Double(Self.pi) / 3, angleFromDegrees.radians) } -// static func trigonometricFunctionChecks() { -// assertClose(1.1863995522992575361931268186727044683, Angle.acos(0.375).radians) -// assertClose(0.3843967744956390830381948729670469737, Angle.asin(0.375).radians) -// assertClose(0.3587706702705722203959200639264604997, Angle.atan(0.375).radians) -// assertClose(0.54041950027058415544357836460859991, Angle.atan2(y: 0.375, x: 0.625).radians) -// -// assertClose(0.9305076219123142911494767922295555080, cos(Angle(radians: 0.375))) -// assertClose(0.3662725290860475613729093517162641571, sin(Angle(radians: 0.375))) -// assertClose(0.3936265759256327582294137871012180981, tan(Angle(radians: 0.375))) -// } -// - static func specialDegreesTrigonometricFunctionChecks() { + static func inverseTrigonometricFunctionChecks() { + XCTAssertEqual(0, Angle.acos(1).degrees) + XCTAssertEqual(30, Angle.acos(sqrt(3)/2).degrees) + XCTAssertEqual(45, Angle.acos(sqrt(2)/2).degrees) + XCTAssertEqual(60, Angle.acos(0.5).degrees) + XCTAssertEqual(90, Angle.acos(0).degrees) + XCTAssertEqual(120, Angle.acos(-0.5).degrees) + XCTAssertEqual(135, Angle.acos(-sqrt(2)/2).degrees) + XCTAssertEqual(150, Angle.acos(-sqrt(3)/2).degrees) + XCTAssertEqual(180, Angle.acos(-1).degrees) + + XCTAssertEqual(-90, Angle.asin(-1).degrees) + XCTAssertEqual(-60, Angle.asin(-sqrt(3)/2).degrees) + XCTAssertEqual(-45, Angle.asin(-sqrt(2)/2).degrees) + XCTAssertEqual(-30, Angle.asin(-0.5).degrees) + XCTAssertEqual(0, Angle.asin(0).degrees) + XCTAssertEqual(30, Angle.asin(0.5).degrees) + XCTAssertEqual(45, Angle.asin(sqrt(2)/2).degrees) + XCTAssertEqual(60, Angle.asin(sqrt(3)/2).degrees) + XCTAssertEqual(90, Angle.asin(1).degrees) + + XCTAssertEqual(-90, Angle.atan(-.infinity).degrees) + XCTAssertEqual(-60, Angle.atan(-sqrt(3)).degrees) + XCTAssertEqual(-45, Angle.atan(-1).degrees) + XCTAssertEqual(-30, Angle.atan(-sqrt(3)/3).degrees) + XCTAssertEqual(0, Angle.atan(0).degrees) + XCTAssertEqual(30, Angle.atan(sqrt(3)/3).degrees) + XCTAssertEqual(45, Angle.atan(1).degrees) + XCTAssertEqual(60, Angle.atan(sqrt(3)).degrees) + XCTAssertEqual(90, Angle.atan(.infinity).degrees) + + XCTAssertEqual(-150, Angle.atan2(y:-sqrt(3), x:-3).degrees) + XCTAssertEqual(-135, Angle.atan2(y:-1, x:-1).degrees) + XCTAssertEqual(-120, Angle.atan2(y:-sqrt(3), x:-1).degrees) + XCTAssertEqual(-90, Angle.atan2(y:-1, x:0).degrees) + XCTAssertEqual(-60, Angle.atan2(y:-sqrt(3), x:1).degrees) + XCTAssertEqual(-45, Angle.atan2(y:-1, x:1).degrees) + XCTAssertEqual(-30, Angle.atan2(y:-sqrt(3), x:3).degrees) + XCTAssertEqual(0, Angle.atan2(y:0, x:1).degrees) + XCTAssertEqual(30, Angle.atan2(y:sqrt(3), x:3).degrees) + XCTAssertEqual(45, Angle.atan2(y:1, x:1).degrees) + XCTAssertEqual(60, Angle.atan2(y:sqrt(3), x:1).degrees) + XCTAssertEqual(90, Angle.atan2(y:1, x:0).degrees) + XCTAssertEqual(120, Angle.atan2(y:sqrt(3), x:-1).degrees) + XCTAssertEqual(135, Angle.atan2(y:1, x:-1).degrees) + XCTAssertEqual(150, Angle.atan2(y:sqrt(3), x:-3).degrees) + XCTAssertEqual(180, Angle.atan2(y:0, x:-1).degrees) + + assertClose(1.1863995522992575361931268186727044683, Angle.acos(0.375).radians) + assertClose(0.3843967744956390830381948729670469737, Angle.asin(0.375).radians) + assertClose(0.3587706702705722203959200639264604997, Angle.atan(0.375).radians) + assertClose(0.54041950027058415544357836460859991, Angle.atan2(y: 0.375, x: 0.625).radians) + } + + static func trigonometricFunctionChecks() { XCTAssertEqual(1, cos(Angle(degrees: -360))) XCTAssertEqual(0, cos(Angle(degrees: -270))) XCTAssertEqual(-1, cos(Angle(degrees: -180))) @@ -103,6 +146,10 @@ where Self: BinaryFloatingPoint { XCTAssertEqual(0, tan(Angle(degrees: 180))) XCTAssertEqual(-.infinity, tan(Angle(degrees: 270))) XCTAssertEqual(0, tan(Angle(degrees: 360))) + + assertClose(0.9305076219123142911494767922295555080, cos(Angle(radians: 0.375))) + assertClose(0.3662725290860475613729093517162641571, sin(Angle(radians: 0.375))) + assertClose(0.3936265759256327582294137871012180981, tan(Angle(radians: 0.375))) } static func additiveArithmeticTests() { @@ -145,8 +192,8 @@ where Self: BinaryFloatingPoint { final class AngleTests: XCTestCase { private func execute(`for` Type: T.Type) { Type.conversionBetweenRadiansAndDegreesChecks() -// Type.trigonometricFunctionChecks() - Type.specialDegreesTrigonometricFunctionChecks() + Type.inverseTrigonometricFunctionChecks() + Type.trigonometricFunctionChecks() Type.additiveArithmeticTests() // Type.rangeContainmentTests() } From 9e7fb846f23c3857b4ea7e9b41a964f49de251fe Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 20 Dec 2020 01:33:02 +0100 Subject: [PATCH 20/22] range containment and distance checks --- Sources/RealModule/Angle.swift | 92 +++++++++++++++++++++----------- Tests/RealTests/AngleTests.swift | 49 ++++++++++++----- 2 files changed, 97 insertions(+), 44 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index 1997ed31..2540d3a5 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -269,30 +269,64 @@ extension Angle { } } -// -//extension Angle { -// /// Checks whether the current angle is contained within a given closed range. -// /// -// /// - Parameters: -// /// -// /// - range: The closed angular range within which containment is checked. -// public func contained(in range: ClosedRange>) -> Bool { -// range.contains(self) -// } -// -// /// Checks whether the current angle is contained within a given half-open range. -// /// -// /// - Parameters: -// /// -// /// - range: The half-open angular range within which containment is checked. -// public func contained(in range: Range>) -> Bool { -// range.contains(self) -// } -//} -// -//extension Angle { -// // “Is angle δ no more than angle ε away from angle ζ?” -//} + +extension Angle { + /// Checks whether the current angle is contained within a range, defined from a start and end angle. + /// + /// The comparison is performed based on the equivalent normalized angles in [-π, π]. + /// + /// Examples: + /// + /// ```swift + /// let angle = Angle(degrees: 175) + /// + /// // returns true + /// angle.isInRange(start: Angle(degrees: 170), end:Angle(degrees: -170)) + /// + /// // returns false + /// angle.isInRange(start: Angle(degrees: -170), end:Angle(degrees: 170)) + /// + /// // returns true + /// angle.isInRange(start: Angle(degrees: 170), end:Angle(degrees: 180)) + /// + /// // returns false + /// angle.isInRange(start: Angle(degrees: 30), end:Angle(degrees: 60)) + /// ``` + /// + /// - Parameters: + /// + /// - start: The start of the range, within which containment is checked. + /// + /// - end: The end of the range, within which containment is checked. + public func isInRange(start: Angle, end: Angle) -> Bool { + let fullNormalized = normalize(value: degrees, limit: 180) + let normalizedStart = normalize(value: start.degrees, limit: 180) + var normalizedEnd = normalize(value: end.degrees, limit: 180) + if normalizedEnd < normalizedStart { + normalizedEnd += 360 + } + return (normalizedStart <= fullNormalized && fullNormalized <= normalizedEnd) + || (normalizedStart <= fullNormalized + 360 && fullNormalized + 360 <= normalizedEnd) + } +} + +extension Angle { + /// Checks whether the current angle is close to another angle within a given tolerance + /// + /// - Precondition: `tolerance` must positive, otherwise the return value is always false + /// + /// - Parameters: + /// + /// - other: the angle from which the distance is controlled. + /// + /// - tolerance: the tolerance around `other` for which the result will be true + /// + /// - Returns: `true` if the current angle falls within the range ```[self - tolerance, self + tolerance]```, otherwise false + public func isClose(to other: Angle, tolerance: Angle) -> Bool { + precondition(tolerance.degrees >= 0) + return isInRange(start: other - tolerance, end: other + tolerance) + } +} extension Angle: Comparable { public static func < (lhs: Angle, rhs: Angle) -> Bool { @@ -340,15 +374,11 @@ private struct DegreesSummands { extension Angle { fileprivate func normalizedDegreesPart() -> T { - normalize(\.degreesPart, limit: 180) - } - - fileprivate func normalizedRadiansPart() -> T { - normalize(\.radiansPart, limit: .pi) + normalize(value: degreesPart, limit: 180) } - private func normalize(_ path: KeyPath, limit: T) -> T { - var normalized = self[keyPath: path] + fileprivate func normalize(value: T, limit: T) -> T { + var normalized = value while normalized > limit { normalized -= 2 * limit diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 7834dba3..40ac1ed1 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -175,18 +175,40 @@ where Self: BinaryFloatingPoint { angle /= 6 XCTAssertEqual(Angle(degrees: 10), angle) } -// -// static func rangeContainmentTests() { -// let angle = Angle(degrees: 30) -// XCTAssertTrue(angle.contained(in: Angle(degrees: 10)...Angle(degrees: 40))) -// XCTAssertTrue(angle.contained(in: Angle(degrees: 10)...Angle(degrees: 30))) -// XCTAssertTrue(angle.contained(in: Angle(degrees: 30)...Angle(degrees: 40))) -// XCTAssertFalse(angle.contained(in: Angle(degrees: 10)...Angle(degrees: 20))) -// XCTAssertFalse(angle.contained(in: Angle(degrees: 50)...Angle(degrees: 60))) -// XCTAssertTrue(angle.contained(in: Angle(degrees: 30)..(degrees: 2))) + XCTAssertFalse(angle175Deg.isClose(to: angle170Deg, tolerance: Angle(degrees: 2))) + + XCTAssertTrue(angle170Deg.isClose(to: angle175Deg, tolerance: Angle(degrees: 10))) + XCTAssertTrue(angle175Deg.isClose(to: angle170Deg, tolerance: Angle(degrees: 5))) + + XCTAssertTrue(angleMinus170Deg.isClose(to: angle175Deg, tolerance: Angle(degrees: 20))) + XCTAssertFalse(angleMinus170Deg.isClose(to: angle175Deg, tolerance: Angle(degrees: 10))) + } } final class AngleTests: XCTestCase { @@ -195,7 +217,8 @@ final class AngleTests: XCTestCase { Type.inverseTrigonometricFunctionChecks() Type.trigonometricFunctionChecks() Type.additiveArithmeticTests() -// Type.rangeContainmentTests() + Type.rangeContainmentTests() + Type.distanceChecks() } From 174391434ecfc472dc780d95b14d5e357822b59b Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 20 Dec 2020 01:57:54 +0100 Subject: [PATCH 21/22] documentation --- Sources/RealModule/Angle.swift | 62 ++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index 2540d3a5..4bd51b29 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -105,9 +105,26 @@ extension Angle { extension ElementaryFunctions where Self: Real { + /// The cos of the angle. + /// + /// The degrees and radians parts are treated separately and then combined together + /// using standard trigonometric [identities]. + /// + /// If possible, the degrees part is split into two subparts; one with exact trigonometric results + /// and another one, whose trigonometric value is calculated by using the equivalent radians + /// representation. + /// + /// This is done recursively, until the only subpart left is the one for which no known, exact + /// trigonometric value exists. + /// + /// Examples: + /// ```cos(Angle(degrees: 126)``` + /// is split recursively in 90° (exact results), 30° (exact results) and 6° (0.10471975512 radians) + /// /// See also: /// - /// `ElementaryFunctions.cos()` + /// [identities]: https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities public static func cos(_ angle: Angle) -> Self { let degrees = angle.normalizedDegreesPart() let cosa = cosd(degrees) @@ -117,6 +134,7 @@ where Self: Real { return cossum(cosa, cosb, sina, sinb) } + /// The cos of an angle in degrees, recursively searching in exact, tabulated results. private static func cosd(_ degrees: Self) -> Self { let (exactPart, rest) = degrees.extractParts() guard let exactAngle = exactPart else { @@ -135,9 +153,26 @@ where Self: Real { extension ElementaryFunctions where Self: Real { + /// The cosine of the angle. + /// + /// The degrees and radians parts are treated separately and then combined together + /// using standard trigonometric [identities]. + /// + /// If possible, the degrees part is split into two subparts; one with exact trigonometric results + /// and another one, whose trigonometric value is calculated by using the equivalent radians + /// representation. + /// + /// This is done recursively, until the only subpart left is the one for which no known, exact + /// trigonometric value exists. + /// + /// Examples: + /// ```cos(Angle(degrees: 126)``` + /// is split recursively in 90° (exact results), 30° (exact results) and 6° (0.10471975512 radians) + /// /// See also: /// - /// `ElementaryFunctions.sin()` + /// [identities]: https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities public static func sin(_ angle: Angle) -> Self { let degrees = angle.normalizedDegreesPart() let cosa = cosd(degrees) @@ -147,6 +182,7 @@ where Self: Real { return sinsum(cosa, cosb, sina, sinb) } + /// The sin of an angle in degrees, recursively searching in exact, tabulated results. private static func sind(_ degrees: Self) -> Self { let (exactPart, rest) = degrees.extractParts() guard let exactAngle = exactPart else { @@ -165,9 +201,26 @@ where Self: Real { extension ElementaryFunctions where Self: Real { + /// The cosine of the angle. + /// + /// The degrees and radians parts are treated separately and then combined together + /// using standard trigonometric [identities]. + /// + /// If possible, the degrees part is split into two subparts; one with exact trigonometric results + /// and another one, whose trigonometric value is calculated by using the equivalent radians + /// representation. + /// + /// This is done recursively, until the only subpart left is the one for which no known, exact + /// trigonometric value exists. + /// + /// Examples: + /// ```cos(Angle(degrees: 126)``` + /// is split recursively in 90° (exact results), 30° (exact results) and 6° (0.10471975512 radians) + /// /// See also: /// - /// `ElementaryFunctions.tan()` + /// [identities]: https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities public static func tan(_ angle: Angle) -> Self { let degrees = angle.normalizedDegreesPart() let tana = tand(degrees) @@ -175,6 +228,7 @@ where Self: Real { return tansum(tana, tanb) } + /// The tan of an angle in degrees, recursively searching in exact, tabulated results. private static func tand(_ degrees: Self) -> Self { let (exactPart, rest) = degrees.extractParts() guard let exactAngle = exactPart else { @@ -244,12 +298,16 @@ extension Angle { return .degrees(exactSolution.degrees * x.realSign) } + /// The 2-argument atan function. + /// + ///- Precondition: `x` and `y` cannot be both 0 at the same time + /// /// See also: /// - /// `RealFunctions.atan2()` public static func atan2(y: T, x: T) -> Self { - let norm = (x*x + y*y).squareRoot() - assert(norm != 0) + let norm = (x * x + y * y).squareRoot() + precondition(norm != 0) let sin = y / norm let cos = x / norm guard let exactSolution = exactAngleConversions().first(where: { cos.magnitude.isApproximatelyEqual(to: $0.cos) From 56c5ffc3e388ba17708fff288479998864829a60 Mon Sep 17 00:00:00 2001 From: jkaliak Date: Sun, 21 Mar 2021 01:08:53 +0100 Subject: [PATCH 22/22] Angle type with trig-pi branch (#85) --- Sources/RealModule/Angle.swift | 289 ++++-------------- Sources/RealModule/Double+Real.swift | 15 + Sources/RealModule/Float+Real.swift | 15 + Sources/RealModule/Real.swift | 114 ++++++- Sources/RealModule/RealFunctions.swift | 207 ++++++++++--- .../_NumericsShims/include/_NumericsShims.h | 30 ++ Tests/RealTests/AngleTests.swift | 104 ++++--- .../RealTests/ElementaryFunctionChecks.swift | 8 +- 8 files changed, 451 insertions(+), 331 deletions(-) diff --git a/Sources/RealModule/Angle.swift b/Sources/RealModule/Angle.swift index 4bd51b29..b951b09a 100644 --- a/Sources/RealModule/Angle.swift +++ b/Sources/RealModule/Angle.swift @@ -15,6 +15,9 @@ /// All trigonometric functions expect the argument to be passed as radians (Real), but this is not enforced by the type system. /// This type serves exactly this purpose, and can be seen as an alternative to the underlying Real implementation. public struct Angle: Equatable { + fileprivate var degreesPart: T = 0 + fileprivate var radiansPart: T = 0 + fileprivate init() {} fileprivate init(degreesPart: T, radiansPart: T) { @@ -39,16 +42,12 @@ public struct Angle: Equatable { } public var radians: T { - radiansPart + degreesPart.asRadians + radiansPart + degreesPart * .pi / 180 } public var degrees: T { - radiansPart.asDegrees + degreesPart + radiansPart * 180 / .pi + degreesPart } - - fileprivate var degreesPart: T = 0 - - fileprivate var radiansPart: T = 0 } extension Angle: AdditiveArithmetic { @@ -103,143 +102,88 @@ extension Angle { } } +private extension Angle { + var piTimesFromDegrees: T { + if degreesPart.magnitude <= 180 { + return degreesPart / 180 + } + let remainder = degreesPart.remainder(dividingBy: 180) + return remainder / 180 + } + + var piTimesFromRadians: T { + if radiansPart.magnitude <= .pi { + return radiansPart / .pi + } + let remainder = radiansPart.remainder(dividingBy: .pi) + return remainder / .pi + } +} + extension ElementaryFunctions where Self: Real { /// The cos of the angle. /// /// The degrees and radians parts are treated separately and then combined together - /// using standard trigonometric [identities]. - /// - /// If possible, the degrees part is split into two subparts; one with exact trigonometric results - /// and another one, whose trigonometric value is calculated by using the equivalent radians - /// representation. - /// - /// This is done recursively, until the only subpart left is the one for which no known, exact - /// trigonometric value exists. - /// - /// Examples: - /// ```cos(Angle(degrees: 126)``` - /// is split recursively in 90° (exact results), 30° (exact results) and 6° (0.10471975512 radians) + /// using standard trigonometric [identities]. For each part, the corresponding remainder + /// by pi or 180° is found, and the higher precision `cos(piTimes:)` function is used /// /// See also: /// - /// `ElementaryFunctions.cos()` /// [identities]: https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities public static func cos(_ angle: Angle) -> Self { - let degrees = angle.normalizedDegreesPart() - let cosa = cosd(degrees) - let cosb = cos(angle.radiansPart) - let sina = sind(degrees) - let sinb = sin(angle.radiansPart) - return cossum(cosa, cosb, sina, sinb) - } - - /// The cos of an angle in degrees, recursively searching in exact, tabulated results. - private static func cosd(_ degrees: Self) -> Self { - let (exactPart, rest) = degrees.extractParts() - guard let exactAngle = exactPart else { - return cos(rest.asRadians) - } - let (cosa, sina) = getExactCosAndSin(for: exactAngle) - let cosb = cosd(rest) - let sinb = sind(rest) - return cossum(cosa, cosb, sina, sinb) - } - - private static func cossum(_ cosa: Self, _ cosb: Self, _ sina: Self, _ sinb: Self) -> Self { + let piTimesDegrees = angle.piTimesFromDegrees + let piTimesRadians = angle.piTimesFromRadians + let cosa = cos(piTimes: piTimesDegrees) + let cosb = cos(piTimes: piTimesRadians) + let sina = sin(piTimes: piTimesDegrees) + let sinb = sin(piTimes: piTimesRadians) return cosa * cosb - sina * sinb } } extension ElementaryFunctions where Self: Real { - /// The cosine of the angle. - /// - /// The degrees and radians parts are treated separately and then combined together - /// using standard trigonometric [identities]. - /// - /// If possible, the degrees part is split into two subparts; one with exact trigonometric results - /// and another one, whose trigonometric value is calculated by using the equivalent radians - /// representation. + /// The sine of the angle. /// - /// This is done recursively, until the only subpart left is the one for which no known, exact - /// trigonometric value exists. /// - /// Examples: - /// ```cos(Angle(degrees: 126)``` - /// is split recursively in 90° (exact results), 30° (exact results) and 6° (0.10471975512 radians) + /// The degrees and radians parts are treated separately and then combined together + /// using standard trigonometric [identities]. For each part, the corresponding remainder + /// by pi or 180° is found, and the higher precision `sin(piTimes:)` function is used /// /// See also: /// - /// `ElementaryFunctions.sin()` /// [identities]: https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities public static func sin(_ angle: Angle) -> Self { - let degrees = angle.normalizedDegreesPart() - let cosa = cosd(degrees) - let cosb = cos(angle.radiansPart) - let sina = sind(degrees) - let sinb = sin(angle.radiansPart) - return sinsum(cosa, cosb, sina, sinb) - } - - /// The sin of an angle in degrees, recursively searching in exact, tabulated results. - private static func sind(_ degrees: Self) -> Self { - let (exactPart, rest) = degrees.extractParts() - guard let exactAngle = exactPart else { - return sin(rest.asRadians) - } - let (cosa, sina) = getExactCosAndSin(for: exactAngle) - let cosb = cosd(rest) - let sinb = sind(rest) - return sinsum(cosa, cosb, sina, sinb) - } - - private static func sinsum(_ cosa: Self, _ cosb: Self, _ sina: Self, _ sinb: Self) -> Self { + let piTimesDegrees = angle.piTimesFromDegrees + let piTimesRadians = angle.piTimesFromRadians + let cosa = cos(piTimes: piTimesDegrees) + let cosb = cos(piTimes: piTimesRadians) + let sina = sin(piTimes: piTimesDegrees) + let sinb = sin(piTimes: piTimesRadians) return sina * cosb + cosa * sinb } } extension ElementaryFunctions where Self: Real { - /// The cosine of the angle. + /// The tangent of the angle. /// /// The degrees and radians parts are treated separately and then combined together - /// using standard trigonometric [identities]. - /// - /// If possible, the degrees part is split into two subparts; one with exact trigonometric results - /// and another one, whose trigonometric value is calculated by using the equivalent radians - /// representation. - /// - /// This is done recursively, until the only subpart left is the one for which no known, exact - /// trigonometric value exists. - /// - /// Examples: - /// ```cos(Angle(degrees: 126)``` - /// is split recursively in 90° (exact results), 30° (exact results) and 6° (0.10471975512 radians) + /// using standard trigonometric [identities]. For each part, the corresponding remainder + /// by pi or 180° is found, and the higher precision `tan(piTimes:)` function is used /// /// See also: /// - /// `ElementaryFunctions.tan()` /// [identities]: https://en.wikipedia.org/wiki/List_of_trigonometric_identities#Angle_sum_and_difference_identities public static func tan(_ angle: Angle) -> Self { - let degrees = angle.normalizedDegreesPart() - let tana = tand(degrees) - let tanb = tan(angle.radiansPart) - return tansum(tana, tanb) - } - - /// The tan of an angle in degrees, recursively searching in exact, tabulated results. - private static func tand(_ degrees: Self) -> Self { - let (exactPart, rest) = degrees.extractParts() - guard let exactAngle = exactPart else { - return tan(rest.asRadians) - } - let tana = getExactTan(for: exactAngle) - let tanb = tand(rest) - return tansum(tana, tanb) - } - - private static func tansum(_ tana: Self, _ tanb: Self) -> Self { + let piTimesDegrees = angle.piTimesFromDegrees + let piTimesRadians = angle.piTimesFromRadians + let tana = tan(piTimes: piTimesDegrees) + let tanb = tan(piTimes: piTimesRadians) switch (tana.isFinite, tanb) { case (false, 0): return tana @@ -251,51 +195,26 @@ where Self: Real { } } -extension ElementaryFunctions -where Self: Real { - fileprivate static func getExactCosAndSin(for degrees: Self) -> (cos: Self, sin: Self) { - let knownTrigonometry = exactAngleConversions().first(where: { $0.degrees == degrees.magnitude })! - return (knownTrigonometry.cos, knownTrigonometry.sin * degrees.realSign) - } - - fileprivate static func getExactTan(for degrees: Self) -> Self { - let knownTrigonometry = exactAngleConversions().first(where: { $0.degrees == degrees.magnitude })! - return knownTrigonometry.tan * degrees.realSign - } -} - extension Angle { /// See also: /// - /// `ElementaryFunctions.acos()` public static func acos(_ x: T) -> Self { - guard let exactSolution = exactAngleConversions().first(where: {$0.cos == x.magnitude }) else { - return .radians(T.acos(x)) - } - let degrees = x > 0 - ? exactSolution.degrees - : 180 - exactSolution.degrees - return .degrees(degrees) + .radians(T.acos(x)) } /// See also: /// - /// `ElementaryFunctions.asin()` public static func asin(_ x: T) -> Self { - guard let exactSolution = exactAngleConversions().first(where: { $0.sin == x.magnitude }) else { - return .radians(T.asin(x)) - } - return .degrees(exactSolution.degrees * x.realSign) + .radians(T.asin(x)) } /// See also: /// - /// `ElementaryFunctions.atan()` public static func atan(_ x: T) -> Self { - guard let exactSolution = exactAngleConversions().first(where: { $0.tan == x.magnitude }) else { - return .radians(T.atan(x)) - } - return .degrees(exactSolution.degrees * x.realSign) + .radians(T.atan(x)) } /// The 2-argument atan function. @@ -306,32 +225,14 @@ extension Angle { /// - /// `RealFunctions.atan2()` public static func atan2(y: T, x: T) -> Self { - let norm = (x * x + y * y).squareRoot() - precondition(norm != 0) - let sin = y / norm - let cos = x / norm - guard let exactSolution = exactAngleConversions().first(where: { cos.magnitude.isApproximatelyEqual(to: $0.cos) - && sin.magnitude.isApproximatelyEqual(to:$0.sin) }) else { - return .radians(T.atan2(y: y, x: x)) - } - if y >= 0 && x >= 0 { - return .degrees(exactSolution.degrees) - } - if y >= 0 && x < 0 { - return .degrees(180 - exactSolution.degrees) - } - if y < 0 && x >= 0 { - return .degrees(-exactSolution.degrees) - } - return .degrees(-180 + exactSolution.degrees) + .radians(T.atan2(y: y, x: x)) } } - extension Angle { /// Checks whether the current angle is contained within a range, defined from a start and end angle. /// - /// The comparison is performed based on the equivalent normalized angles in [-π, π]. + /// The comparison is performed based on the equivalent normalized angles in [-pi, pi]. /// /// Examples: /// @@ -364,7 +265,7 @@ extension Angle { normalizedEnd += 360 } return (normalizedStart <= fullNormalized && fullNormalized <= normalizedEnd) - || (normalizedStart <= fullNormalized + 360 && fullNormalized + 360 <= normalizedEnd) + || (normalizedStart <= fullNormalized + 360 && fullNormalized + 360 <= normalizedEnd) } } @@ -380,7 +281,7 @@ extension Angle { /// - tolerance: the tolerance around `other` for which the result will be true /// /// - Returns: `true` if the current angle falls within the range ```[self - tolerance, self + tolerance]```, otherwise false - public func isClose(to other: Angle, tolerance: Angle) -> Bool { + public func isClose(to other: Angle, within tolerance: Angle) -> Bool { precondition(tolerance.degrees >= 0) return isInRange(start: other - tolerance, end: other + tolerance) } @@ -395,46 +296,7 @@ extension Angle: Comparable { } } -extension Real { - fileprivate var asRadians: Self { self * .pi / 180 } - - fileprivate var asDegrees: Self { self * 180 / .pi } - - fileprivate func extractParts() -> (common: Self?, rest: Self) { - if let summandsAbove90 = extractParts(limit: 90) { - return (summandsAbove90.exact, summandsAbove90.rest) - } - if let summandsAbove60 = extractParts(limit: 60) { - return (summandsAbove60.exact, summandsAbove60.rest) - } - if let summandsAbove45 = extractParts(limit: 45) { - return (summandsAbove45.exact, summandsAbove45.rest) - } - if let summandsAbove30 = extractParts(limit: 30) { - return (summandsAbove30.exact, summandsAbove30.rest) - } - return (nil, self) - } - - private func extractParts(limit: Self) -> DegreesSummands? { - guard self.magnitude >= limit else { - return nil - } - return DegreesSummands(exact: realSign * limit, - rest: realSign * (self.magnitude - limit)) - } -} - -private struct DegreesSummands { - let exact: T - let rest: T -} - extension Angle { - fileprivate func normalizedDegreesPart() -> T { - normalize(value: degreesPart, limit: 180) - } - fileprivate func normalize(value: T, limit: T) -> T { var normalized = value @@ -449,44 +311,3 @@ extension Angle { return normalized } } - -private struct ExactTrigonometry { - let degrees: T - let cos: T - let sin: T - let tan: T -} - -private func exactAngleConversions() -> [ExactTrigonometry] { - [ - ExactTrigonometry(degrees: 0, - cos: 1, - sin: 0, - tan: 0), - ExactTrigonometry(degrees: 30, - cos: T.sqrt(3)/2, - sin: 1 / 2, - tan: T.sqrt(3) / 3), - ExactTrigonometry(degrees: 45, - cos: T.sqrt(2) / 2, - sin: T.sqrt(2) / 2, - tan: 1), - ExactTrigonometry(degrees: 60, - cos: 1 / 2, - sin: T.sqrt(3) / 2, - tan: T.sqrt(3)), - ExactTrigonometry(degrees: 90, - cos: 0, - sin: 1, - tan: T.infinity), - ] -} - -extension Real { - fileprivate var realSign: Self { - guard self.isFinite else { - return self == .infinity ? 1 : -1 - } - return self > 0 ? 1 : -1 - } -} diff --git a/Sources/RealModule/Double+Real.swift b/Sources/RealModule/Double+Real.swift index 3e060f3f..e7678b87 100644 --- a/Sources/RealModule/Double+Real.swift +++ b/Sources/RealModule/Double+Real.swift @@ -108,6 +108,21 @@ extension Double: Real { } #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + @_transparent + public static func cos(piTimes x: Double) -> Double { + libm_cospi(x) + } + + @_transparent + public static func sin(piTimes x: Double) -> Double { + libm_sinpi(x) + } + + @_transparent + public static func tan(piTimes x: Double) -> Double { + libm_tanpi(x) + } + @_transparent public static func exp10(_ x: Double) -> Double { libm_exp10(x) diff --git a/Sources/RealModule/Float+Real.swift b/Sources/RealModule/Float+Real.swift index 4147e8b8..e24282fd 100644 --- a/Sources/RealModule/Float+Real.swift +++ b/Sources/RealModule/Float+Real.swift @@ -108,6 +108,21 @@ extension Float: Real { } #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + @_transparent + public static func cos(piTimes x: Float) -> Float { + libm_cospif(x) + } + + @_transparent + public static func sin(piTimes x: Float) -> Float { + libm_sinpif(x) + } + + @_transparent + public static func tan(piTimes x: Float) -> Float { + libm_tanpif(x) + } + @_transparent public static func exp10(_ x: Float) -> Float { libm_exp10f(x) diff --git a/Sources/RealModule/Real.swift b/Sources/RealModule/Real.swift index e582f0c8..b43b57b0 100644 --- a/Sources/RealModule/Real.swift +++ b/Sources/RealModule/Real.swift @@ -34,7 +34,92 @@ public protocol Real: FloatingPoint, RealFunctions, AlgebraicField { // it does allow us to default the implementation of a few operations, // and also provides `signGamma`. extension Real { - // Most math libraries do not provide exp10, so we need a default implementation. + + @_transparent + public static func cos(piTimes x: Self) -> Self { + // Cosine is even, so all we need is the magnitude. + let x = x.magnitude + // If x is not finite, the result is nan. + guard x.isFinite else { return .nan } + // If x is finite and at least .radix / .ulpOfOne, it is an even + // integer, which means that cos(piTimes: x) is 1.0 + if x >= Self(Self.radix) / .ulpOfOne { return 1 } + // Break x up as x = n/2 + f where n is an integer. In binary, the + // following computation is always exact, and trivially gives the + // correct result. + // TODO: analyze and fixup for decimal types + let n = (2*x).rounded(.toNearestOrEven) + let f = x.addingProduct(-1/2, n) + // Because cosine is 2π-periodic, we don't actually care about + // most of n; we only need the two least significant bits of n + // represented as an integer: + let quadrant = n._lowWord & 0x3 + switch quadrant { + case 0: return cos(.pi * f) + case 1: return -sin(.pi * f) + case 2: return -cos(.pi * f) + case 3: return sin(.pi * f) + default: fatalError() + } + } + + @_transparent + public static func sin(piTimes x: Self) -> Self { + // If x is not finite, the result is nan. + guard x.isFinite else { return .nan } + // If x.magnitude is finite and at least 1 / .ulpOfOne, it is an + // integer, which means that sin(piTimes: x) is ±0.0 + if x.magnitude >= 1 / .ulpOfOne { + return Self(signOf: x, magnitudeOf: 0) + } + // Break x up as x = n/2 + f where n is an integer. In binary, the + // following computation is always exact, and trivially gives the + // correct result. + // TODO: analyze and fixup for decimal types + let n = (2*x).rounded(.toNearestOrEven) + let f = x.addingProduct(-1/2, n) + // Because sine is 2π-periodic, we don't actually care about + // most of n; we only need the two least significant bits of n + // represented as an integer: + let quadrant = n._lowWord & 0x3 + switch quadrant { + case 0: return sin(.pi * f) + case 1: return cos(.pi * f) + case 2: return -sin(.pi * f) + case 3: return -cos(.pi * f) + default: fatalError() + } + } + + @_transparent + public static func tan(piTimes x: Self) -> Self { + // If x is not finite, the result is nan. + guard x.isFinite else { return .nan } + // TODO: choose policy for exact 0, 1, infinity cases and implement as + // appropriate. + // If x.magnitude is finite and at least .radix / .ulpOfOne, it is an + // even integer, which means that sin(piTimes: x) is ±0.0 and + // cos(piTimes: x) is 1.0. + if x.magnitude >= Self(Self.radix) / .ulpOfOne { + return Self(signOf: x, magnitudeOf: 0) + } + // Break x up as x = n/2 + f where n is an integer. In binary, the + // following computation is always exact, and trivially gives the + // correct result. + // TODO: analyze and fixup for decimal types + let n = (2*x).rounded(.toNearestOrEven) + let f = x.addingProduct(-1/2, n) + // Because tangent is π-periodic, we don't actually care about + // most of n; we only need the least significant bit of n represented + // as an integer: + let sector = n._lowWord & 0x1 + switch sector { + case 0: return tan(.pi * f) + case 1: return -1/tan(.pi * f) + default: fatalError() + } + } + @_transparent public static func exp10(_ x: Self) -> Self { return pow(10, x) @@ -57,10 +142,10 @@ extension Real { if x >= 0 { return .plus } // For negative x, we arbitrarily choose to assign a sign of .plus to the // poles. - let trunc = x.rounded(.towardZero) - if x == trunc { return .plus } + let integralPart = x.rounded(.towardZero) + if x == integralPart { return .plus } // Otherwise, signGamma is .minus if the integral part of x is even. - return trunc.isEven ? .minus : .plus + return integralPart.isEven ? .minus : .plus } // Determines if this value is even, assuming that it is an integer. @@ -106,3 +191,24 @@ extension Real { return nil } } + +// MARK: Implementation details +extension Real where Self: BinaryFloatingPoint { + @_transparent + public var _lowWord: UInt { + // If magnitude is small enough, we can simply convert to Int64 and then + // wrap to UInt. + if magnitude < 0x1.0p63 { + return UInt(truncatingIfNeeded: Int64(self.rounded(.down))) + } + precondition(isFinite) + // Clear any bits above bit 63; the result of this expression is + // strictly in the range [0, 0x1p64). (Note that if we had not eliminated + // small magnitudes already, the range would include tiny negative values + // which would then produce the wrong result; the branch above is not + // only for performance. + let cleared = self - 0x1p64*(self * 0x1p-64).rounded(.down) + // Now we can unconditionally convert to UInt64, and then wrap to UInt. + return UInt(truncatingIfNeeded: UInt64(cleared)) + } +} diff --git a/Sources/RealModule/RealFunctions.swift b/Sources/RealModule/RealFunctions.swift index e6fed7d2..8350920a 100644 --- a/Sources/RealModule/RealFunctions.swift +++ b/Sources/RealModule/RealFunctions.swift @@ -2,7 +2,7 @@ // // This source file is part of the Swift Numerics open source project // -// Copyright (c) 2019 Apple Inc. and the Swift Numerics project authors +// Copyright (c) 2019-2020 Apple Inc. and the Swift Numerics project authors // Licensed under Apache License v2.0 with Runtime Library Exception // // See https://swift.org/LICENSE.txt for license information @@ -10,92 +10,212 @@ //===----------------------------------------------------------------------===// public protocol RealFunctions: ElementaryFunctions { - /// `atan(y/x)`, with sign selected according to the quadrant of `(x, y)`. + + /// `atan(y/x)`, with representative selected using the quadrant`(x,y)`. + /// + /// The [atan2 function][wiki] computes the angle (in radians, in the + /// range [-π, π]) formed between the positive real axis and the point + /// `(x,y)`. The sign of the result always matches the sign of y. + /// + /// - Warning: + /// Note the parameter ordering of this function; the `y` parameter + /// comes *before* the `x` parameter. This is a historical curiosity + /// going back to early FORTRAN math libraries. In order to minimize + /// opportunities for confusion and subtle bugs, we require explicit + /// parameter labels with this function. /// /// See also: /// - - /// - `atan()` + /// - `ElementaryFunctions.atan(_:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Atan2 static func atan2(y: Self, x: Self) -> Self - /// The error function evaluated at `x`. + /// `cos(πx)` + /// + /// Computes the cosine of π times `x`. + /// + /// Because π is not representable in any `FloatingPoint` type, for large + /// `x`, `.cos(.pi * x)` can have arbitrarily large relative error; + /// `.cos(piTimes: x)` always provides a result with small relative error. + /// + /// This is observable even for modest arguments; consider `0.5`: + /// ```swift + /// Float.cos(.pi * 0.5) // 7.54979e-08 + /// Float.cos(piTimes: 0.5) // 0.0 + /// ``` + /// It's important to be clear that there is no bug in the example + /// given above. Every step of both computations is producing the most + /// accurate possible result. + /// + /// Symmetries: + /// - + /// - `.cos(piTimes: -x) = .cos(piTimes: x)`. /// /// See also: /// - - /// - `erfc()` + /// - `sin(piTimes:)` + /// - `tan(piTimes:)` + /// - `ElementaryFunctions.cos(_:)` + static func cos(piTimes x: Self) -> Self + + /// `sin(πx)` + /// + /// Computes the sine of π times `x`. + /// + /// Because π is not representable in any `FloatingPoint` type, for large + /// `x`, `.sin(.pi * x)` can have arbitrarily large relative error; + /// `.sin(piTimes: x)` always provides a result with small relative error. + /// + /// This is observable even for modest arguments; consider `10`: + /// ```swift + /// Float.sin(.pi * 10) // -2.4636322e-06 + /// Float.sin(piTimes: 10) // 0.0 + /// ``` + /// It's important to be clear that there is no bug in the example + /// given above. Every step of both computations is producing the most + /// accurate possible result. + /// + /// Symmetry: + /// - + /// `.sin(piTimes: -x) = -.sin(piTimes: x)`. + /// + /// See also: + /// - + /// - `cos(piTimes:)` + /// - `tan(piTimes:)` + /// - `ElementaryFunctions.sin(_:)` + static func sin(piTimes x: Self) -> Self + + /// `tan(πx)` + /// + /// Computes the tangent of π times `x`. + /// + /// Because π is not representable in any `FloatingPoint` type, for large + /// `x`, `.tan(.pi * x)` can have arbitrarily large relative error; + /// `.tan(piTimes: x)` always provides a result with small relative error. + /// + /// This is observable even for modest arguments; consider `0.5`: + /// ```swift + /// Float.tan(.pi * 0.5) // 13245402.0 + /// Float.tan(piTimes: 0.5) // infinity + /// ``` + /// It's important to be clear that there is no bug in the example + /// given above. Every step of both computations is producing the most + /// accurate possible result. + /// + /// Symmetry: + /// - + /// `.tan(piTimes: -x) = -.tan(piTimes: x)`. + /// + /// See also: + /// - + /// - `cos(piTimes:)` + /// - `sin(piTimes:)` + /// - `ElementaryFunctions.tan(_:)` + static func tan(piTimes x: Self) -> Self + + /// The [error function][wiki] evaluated at `x`. + /// + /// See also: + /// - + /// - `erfc(_:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Error_function static func erf(_ x: Self) -> Self - /// The complimentary error function evaluated at `x`. + /// The complimentary [error function][wiki] evaluated at `x`. /// /// See also: /// - - /// - `erf()` + /// - `erf(_:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Error_function static func erfc(_ x: Self) -> Self - /// 2^x + /// 2ˣ /// /// See also: /// - - /// - `exp()` - /// - `expMinusOne()` - /// - `exp10()` - /// - `log2()` - /// - `pow()` + /// - `ElementaryFunctions.exp(_:)` + /// - `ElementaryFunctions.expMinusOne(_:)` + /// - `exp10(_:)` + /// - `log2(_:)` + /// - `ElementaryFunctions.pow(_:)` static func exp2(_ x: Self) -> Self - /// 10^x + /// 10ˣ /// /// See also: /// - - /// - `exp()` - /// - `expMinusOne()` - /// - `exp2()` - /// - `log10()` - /// - `pow()` + /// - `ElementaryFunctions.exp(_:)` + /// - `ElementaryFunctions.expMinusOne(_:)` + /// - `exp2(_:)` + /// - `log10(_:)` + /// - `ElementaryFunctions.pow(_:)` static func exp10(_ x: Self) -> Self - /// `sqrt(x*x + y*y)`, computed in a manner that avoids spurious overflow or underflow. + /// The square root of the sum of squares of `x` and `y`. + /// + /// The naive expression `.sqrt(x*x + y*y)` and overflow + /// or underflow if `x` or `y` is not well-scaled, producing zero or + /// infinity, even when the mathematical result is representable. + /// + /// The [hypot][wiki] takes care to avoid this, and always + /// produces an accurate result when one is available. + /// + /// See also: + /// - + /// - `ElementaryFunctions.sqrt(_:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Hypot static func hypot(_ x: Self, _ y: Self) -> Self - /// The gamma function Γ(x). + /// The [gamma function][wiki] Γ(x). /// /// See also: /// - - /// - `logGamma()` - /// - `signGamma()` + /// - `logGamma(_:)` + /// - `signGamma(_:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Gamma_function static func gamma(_ x: Self) -> Self /// The base-2 logarithm of `x`. /// /// See also: /// - - /// - `exp2()` - /// - `log()` - /// - `log(onePlus:)` - /// - `log10()` + /// - `exp2(_:)` + /// - `ElementaryFunctions.log(_:)` + /// - `ElementaryFunctions.log(onePlus:)` + /// - `log10(_:)` static func log2(_ x: Self) -> Self /// The base-10 logarithm of `x`. /// /// See also: /// - - /// - `exp10()` - /// - `log()` - /// - `log(onePlus:)` - /// - `log2()` + /// - `exp10(_:)` + /// - `ElementaryFunctions.log(_:)` + /// - `ElementaryFunctions.log(onePlus:)` + /// - `log2(_:)` static func log10(_ x: Self) -> Self #if !os(Windows) - /// The logarithm of the absolute value of the gamma function, log(|Γ(x)|). + /// The logarithm of the absolute value of the [gamma function][wiki], log(|Γ(x)|). /// - /// Not available on Windows targets. + /// - Warning: + /// Not available on Windows. /// /// See also: /// - - /// - `gamma()` - /// - `signGamma()` + /// - `gamma(_:)` + /// - `signGamma(_:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Gamma_function static func logGamma(_ x: Self) -> Self - /// The sign of the gamma function, Γ(x). + /// The sign of the [gamma function][wiki], Γ(x). /// /// For `x >= 0`, `signGamma(x)` is `.plus`. For negative `x`, `signGamma(x)` is `.plus` /// when `x` is an integer, and otherwise it is `.minus` whenever `trunc(x)` is even, and `.plus` @@ -104,12 +224,15 @@ public protocol RealFunctions: ElementaryFunctions { /// This function is used together with `logGamma`, which computes the logarithm of the /// absolute value of Γ(x), to recover the sign information. /// - /// Not available on Windows targets. + /// - Warning: + /// Not available on Windows. /// /// See also: /// - - /// - `gamma()` - /// - `logGamma()` + /// - `gamma(_:)` + /// - `logGamma(_:)` + /// + /// [wiki]: https://en.wikipedia.org/wiki/Gamma_function static func signGamma(_ x: Self) -> FloatingPointSign #endif @@ -117,4 +240,10 @@ public protocol RealFunctions: ElementaryFunctions { /// /// Whichever is faster should be chosen by the compiler statically. static func _mulAdd(_ a: Self, _ b: Self, _ c: Self) -> Self + + // MARK: Implementation details + + /// The low-word of the integer formed by truncating this value. + var _lowWord: UInt { get } } + diff --git a/Sources/_NumericsShims/include/_NumericsShims.h b/Sources/_NumericsShims/include/_NumericsShims.h index 58b12795..c7094f3f 100644 --- a/Sources/_NumericsShims/include/_NumericsShims.h +++ b/Sources/_NumericsShims/include/_NumericsShims.h @@ -116,6 +116,21 @@ HEADER_SHIM float libm_exp2f(float x) { } #if __APPLE__ +HEADER_SHIM float libm_cospif(float x) { + extern float __cospif(float); + return __cospif(x); +} + +HEADER_SHIM float libm_sinpif(float x) { + extern float __sinpif(float); + return __sinpif(x); +} + +HEADER_SHIM float libm_tanpif(float x) { + extern float __tanpif(float); + return __tanpif(x); +} + HEADER_SHIM float libm_exp10f(float x) { extern float __exp10f(float); return __exp10f(x); @@ -241,6 +256,21 @@ HEADER_SHIM double libm_exp2(double x) { } #if __APPLE__ +HEADER_SHIM double libm_cospi(double x) { + extern double __cospi(double); + return __cospi(x); +} + +HEADER_SHIM double libm_sinpi(double x) { + extern double __sinpi(double); + return __sinpi(x); +} + +HEADER_SHIM double libm_tanpi(double x) { + extern double __tanpi(double); + return __tanpi(x); +} + HEADER_SHIM double libm_exp10(double x) { extern double __exp10(double); return __exp10(x); diff --git a/Tests/RealTests/AngleTests.swift b/Tests/RealTests/AngleTests.swift index 40ac1ed1..d85f1e48 100644 --- a/Tests/RealTests/AngleTests.swift +++ b/Tests/RealTests/AngleTests.swift @@ -17,62 +17,60 @@ import _TestSupport internal extension Real where Self: BinaryFloatingPoint { static func conversionBetweenRadiansAndDegreesChecks() { - let angleFromRadians = Angle(radians: Self.pi / 3) + let angleFromRadians = Angle(radians: .pi / 3) assertClose(60, angleFromRadians.degrees) let angleFromDegrees = Angle(degrees: 120) - // the compiler complains with the following line - // assertClose(2 * Self.pi / 3, angleFromDegrees.radians) - assertClose(2 * Double(Self.pi) / 3, angleFromDegrees.radians) + assertClose(2 * .pi / 3, angleFromDegrees.radians) } static func inverseTrigonometricFunctionChecks() { - XCTAssertEqual(0, Angle.acos(1).degrees) - XCTAssertEqual(30, Angle.acos(sqrt(3)/2).degrees) - XCTAssertEqual(45, Angle.acos(sqrt(2)/2).degrees) - XCTAssertEqual(60, Angle.acos(0.5).degrees) - XCTAssertEqual(90, Angle.acos(0).degrees) - XCTAssertEqual(120, Angle.acos(-0.5).degrees) - XCTAssertEqual(135, Angle.acos(-sqrt(2)/2).degrees) - XCTAssertEqual(150, Angle.acos(-sqrt(3)/2).degrees) - XCTAssertEqual(180, Angle.acos(-1).degrees) + assertClose(0, Angle.acos(1).degrees) + assertClose(30, Angle.acos(sqrt(3)/2).degrees) + assertClose(45, Angle.acos(sqrt(2)/2).degrees) + assertClose(60, Angle.acos(0.5).degrees) + assertClose(90, Angle.acos(0).degrees) + assertClose(120, Angle.acos(-0.5).degrees) + assertClose(135, Angle.acos(-sqrt(2)/2).degrees) + assertClose(150, Angle.acos(-sqrt(3)/2).degrees) + assertClose(180, Angle.acos(-1).degrees) - XCTAssertEqual(-90, Angle.asin(-1).degrees) - XCTAssertEqual(-60, Angle.asin(-sqrt(3)/2).degrees) - XCTAssertEqual(-45, Angle.asin(-sqrt(2)/2).degrees) - XCTAssertEqual(-30, Angle.asin(-0.5).degrees) - XCTAssertEqual(0, Angle.asin(0).degrees) - XCTAssertEqual(30, Angle.asin(0.5).degrees) - XCTAssertEqual(45, Angle.asin(sqrt(2)/2).degrees) - XCTAssertEqual(60, Angle.asin(sqrt(3)/2).degrees) - XCTAssertEqual(90, Angle.asin(1).degrees) + assertClose(-90, Angle.asin(-1).degrees) + assertClose(-60, Angle.asin(-sqrt(3)/2).degrees) + assertClose(-45, Angle.asin(-sqrt(2)/2).degrees) + assertClose(-30, Angle.asin(-0.5).degrees) + assertClose(0, Angle.asin(0).degrees) + assertClose(30, Angle.asin(0.5).degrees) + assertClose(45, Angle.asin(sqrt(2)/2).degrees) + assertClose(60, Angle.asin(sqrt(3)/2).degrees) + assertClose(90, Angle.asin(1).degrees) - XCTAssertEqual(-90, Angle.atan(-.infinity).degrees) - XCTAssertEqual(-60, Angle.atan(-sqrt(3)).degrees) - XCTAssertEqual(-45, Angle.atan(-1).degrees) - XCTAssertEqual(-30, Angle.atan(-sqrt(3)/3).degrees) - XCTAssertEqual(0, Angle.atan(0).degrees) - XCTAssertEqual(30, Angle.atan(sqrt(3)/3).degrees) - XCTAssertEqual(45, Angle.atan(1).degrees) - XCTAssertEqual(60, Angle.atan(sqrt(3)).degrees) - XCTAssertEqual(90, Angle.atan(.infinity).degrees) + assertClose(-90, Angle.atan(-.infinity).degrees) + assertClose(-60, Angle.atan(-sqrt(3)).degrees) + assertClose(-45, Angle.atan(-1).degrees) + assertClose(-30, Angle.atan(-sqrt(3)/3).degrees) + assertClose(0, Angle.atan(0).degrees) + assertClose(30, Angle.atan(sqrt(3)/3).degrees) + assertClose(45, Angle.atan(1).degrees) + assertClose(60, Angle.atan(sqrt(3)).degrees) + assertClose(90, Angle.atan(.infinity).degrees) - XCTAssertEqual(-150, Angle.atan2(y:-sqrt(3), x:-3).degrees) - XCTAssertEqual(-135, Angle.atan2(y:-1, x:-1).degrees) - XCTAssertEqual(-120, Angle.atan2(y:-sqrt(3), x:-1).degrees) - XCTAssertEqual(-90, Angle.atan2(y:-1, x:0).degrees) - XCTAssertEqual(-60, Angle.atan2(y:-sqrt(3), x:1).degrees) - XCTAssertEqual(-45, Angle.atan2(y:-1, x:1).degrees) - XCTAssertEqual(-30, Angle.atan2(y:-sqrt(3), x:3).degrees) - XCTAssertEqual(0, Angle.atan2(y:0, x:1).degrees) - XCTAssertEqual(30, Angle.atan2(y:sqrt(3), x:3).degrees) - XCTAssertEqual(45, Angle.atan2(y:1, x:1).degrees) - XCTAssertEqual(60, Angle.atan2(y:sqrt(3), x:1).degrees) - XCTAssertEqual(90, Angle.atan2(y:1, x:0).degrees) - XCTAssertEqual(120, Angle.atan2(y:sqrt(3), x:-1).degrees) - XCTAssertEqual(135, Angle.atan2(y:1, x:-1).degrees) - XCTAssertEqual(150, Angle.atan2(y:sqrt(3), x:-3).degrees) - XCTAssertEqual(180, Angle.atan2(y:0, x:-1).degrees) + assertClose(-150, Angle.atan2(y:-sqrt(3), x:-3).degrees) + assertClose(-135, Angle.atan2(y:-1, x:-1).degrees) + assertClose(-120, Angle.atan2(y:-sqrt(3), x:-1).degrees) + assertClose(-90, Angle.atan2(y:-1, x:0).degrees) + assertClose(-60, Angle.atan2(y:-sqrt(3), x:1).degrees) + assertClose(-45, Angle.atan2(y:-1, x:1).degrees) + assertClose(-30, Angle.atan2(y:-sqrt(3), x:3).degrees) + assertClose(0, Angle.atan2(y:0, x:1).degrees) + assertClose(30, Angle.atan2(y:sqrt(3), x:3).degrees) + assertClose(45, Angle.atan2(y:1, x:1).degrees) + assertClose(60, Angle.atan2(y:sqrt(3), x:1).degrees) + assertClose(90, Angle.atan2(y:1, x:0).degrees) + assertClose(120, Angle.atan2(y:sqrt(3), x:-1).degrees) + assertClose(135, Angle.atan2(y:1, x:-1).degrees) + assertClose(150, Angle.atan2(y:sqrt(3), x:-3).degrees) + assertClose(180, Angle.atan2(y:0, x:-1).degrees) assertClose(1.1863995522992575361931268186727044683, Angle.acos(0.375).radians) assertClose(0.3843967744956390830381948729670469737, Angle.asin(0.375).radians) @@ -200,14 +198,14 @@ where Self: BinaryFloatingPoint { let angle170Deg = Angle(degrees: 350) + Angle.radians(-Self.pi) let angleMinus170Deg = Angle(degrees: -350) + Angle.radians(Self.pi) - XCTAssertFalse(angle170Deg.isClose(to: angle175Deg, tolerance: Angle(degrees: 2))) - XCTAssertFalse(angle175Deg.isClose(to: angle170Deg, tolerance: Angle(degrees: 2))) + XCTAssertFalse(angle170Deg.isClose(to: angle175Deg, within: Angle(degrees: 2))) + XCTAssertFalse(angle175Deg.isClose(to: angle170Deg, within: Angle(degrees: 2))) - XCTAssertTrue(angle170Deg.isClose(to: angle175Deg, tolerance: Angle(degrees: 10))) - XCTAssertTrue(angle175Deg.isClose(to: angle170Deg, tolerance: Angle(degrees: 5))) + XCTAssertTrue(angle170Deg.isClose(to: angle175Deg, within: Angle(degrees: 10))) + XCTAssertTrue(angle175Deg.isClose(to: angle170Deg, within: Angle(degrees: 5))) - XCTAssertTrue(angleMinus170Deg.isClose(to: angle175Deg, tolerance: Angle(degrees: 20))) - XCTAssertFalse(angleMinus170Deg.isClose(to: angle175Deg, tolerance: Angle(degrees: 10))) + XCTAssertTrue(angleMinus170Deg.isClose(to: angle175Deg, within: Angle(degrees: 20))) + XCTAssertFalse(angleMinus170Deg.isClose(to: angle175Deg, within: Angle(degrees: 10))) } } diff --git a/Tests/RealTests/ElementaryFunctionChecks.swift b/Tests/RealTests/ElementaryFunctionChecks.swift index b3a37042..bc1cab44 100644 --- a/Tests/RealTests/ElementaryFunctionChecks.swift +++ b/Tests/RealTests/ElementaryFunctionChecks.swift @@ -1,4 +1,4 @@ -//===--- ElementaryFunctionChecks.swift ------------------------*- swift -*-===// +//===--- ElementaryFunctionChecks.swift -----------------------*- swift -*-===// // // This source file is part of the Swift Numerics open source project // @@ -87,6 +87,11 @@ internal func assertClose( )) } +extension FloatingPoint { + var isPositiveZero: Bool { self == 0 && sign == .plus } + var isNegativeZero: Bool { self == 0 && sign == .minus } +} + internal extension ElementaryFunctions where Self: BinaryFloatingPoint { static func elementaryFunctionChecks() { assertClose(1.1863995522992575361931268186727044683, Self.acos(0.375)) @@ -157,6 +162,7 @@ final class ElementaryFunctionChecks: XCTestCase { func testFloat80() { Float80.elementaryFunctionChecks() Float80.realFunctionChecks() + Float80.cosPiTests() } #endif }