From cbde4be2015adf6cbdbff849b7df58aeeee1f2e3 Mon Sep 17 00:00:00 2001 From: Joe Newton <> Date: Thu, 20 Feb 2020 10:13:13 -0500 Subject: [PATCH] Added additional primitive functions for floating point complex numbers --- .gitignore | 1 + ATTRIBUTIONS | 216 ++++ Complex.xcodeproj/project.pbxproj | 132 ++ Sources/Complex/Functions.swift | 236 ++++ Sources/Complex/Functions.swift.gyb | 193 +++ Tests/ComplexTests/FunctionsTests.swift | 173 +++ Tests/ComplexTests/FunctionsTests.swift.gyb | 212 ++++ utils/gyb | 3 + utils/gyb.py | 1260 +++++++++++++++++++ 9 files changed, 2426 insertions(+) create mode 100644 ATTRIBUTIONS create mode 100644 Sources/Complex/Functions.swift.gyb create mode 100644 Tests/ComplexTests/FunctionsTests.swift.gyb create mode 100755 utils/gyb create mode 100644 utils/gyb.py diff --git a/.gitignore b/.gitignore index e9bbc3c..a500584 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ Complex.xcodeproj/project.xcworkspace Complex.xcodeproj/xcuserdata .swiftpm Carthage +gyb.pyc diff --git a/ATTRIBUTIONS b/ATTRIBUTIONS new file mode 100644 index 0000000..545b330 --- /dev/null +++ b/ATTRIBUTIONS @@ -0,0 +1,216 @@ +gyb and gyb.py are copied from the Swift project. + +https://github.com/apple/swift +-------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + +## Runtime Library Exception to the Apache 2.0 License: ## + + + As an exception, if you use this Software to compile your source code and + portions of this Software are embedded into the binary product as a result, + you may redistribute such product without providing attribution as would + otherwise be required by Sections 4(a), 4(b) and 4(d) of the License. diff --git a/Complex.xcodeproj/project.pbxproj b/Complex.xcodeproj/project.pbxproj index dd8fccd..f37fa14 100644 --- a/Complex.xcodeproj/project.pbxproj +++ b/Complex.xcodeproj/project.pbxproj @@ -7,6 +7,17 @@ objects = { /* Begin PBXAggregateTarget section */ + DDB8127223FA5C0C0079FEB5 /* Generate Sources */ = { + isa = PBXAggregateTarget; + buildConfigurationList = DDB8127323FA5C0C0079FEB5 /* Build configuration list for PBXAggregateTarget "Generate Sources" */; + buildPhases = ( + DDB8127623FA5C160079FEB5 /* Generate Sources */, + ); + dependencies = ( + ); + name = "Generate Sources"; + productName = "Generate Sources"; + }; DDFEECA423F1BA0B0096015C /* Run SwiftLint */ = { isa = PBXAggregateTarget; buildConfigurationList = DDFEECA523F1BA0B0096015C /* Build configuration list for PBXAggregateTarget "Run SwiftLint" */; @@ -51,6 +62,20 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + DDB8127723FA5C380079FEB5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DDFEEC2A23EF13900096015C /* Project object */; + proxyType = 1; + remoteGlobalIDString = DDB8127223FA5C0C0079FEB5; + remoteInfo = "Generate Sources"; + }; + DDB8127923FA5C3E0079FEB5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DDFEEC2A23EF13900096015C /* Project object */; + proxyType = 1; + remoteGlobalIDString = DDB8127223FA5C0C0079FEB5; + remoteInfo = "Generate Sources"; + }; DDB8127B23FA5C3E0079FEB5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DDFEEC2A23EF13900096015C /* Project object */; @@ -58,6 +83,13 @@ remoteGlobalIDString = DDFEECA423F1BA0B0096015C; remoteInfo = "Run SwiftLint"; }; + DDB8127D23FA5C430079FEB5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DDFEEC2A23EF13900096015C /* Project object */; + proxyType = 1; + remoteGlobalIDString = DDB8127223FA5C0C0079FEB5; + remoteInfo = "Generate Sources"; + }; DDB8127F23FA5C430079FEB5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DDFEEC2A23EF13900096015C /* Project object */; @@ -65,6 +97,13 @@ remoteGlobalIDString = DDFEECA423F1BA0B0096015C; remoteInfo = "Run SwiftLint"; }; + DDB8128123FA5C480079FEB5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DDFEEC2A23EF13900096015C /* Project object */; + proxyType = 1; + remoteGlobalIDString = DDB8127223FA5C0C0079FEB5; + remoteInfo = "Generate Sources"; + }; DDB8128323FA5C480079FEB5 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = DDFEEC2A23EF13900096015C /* Project object */; @@ -113,6 +152,11 @@ DDB8122023F656AC0079FEB5 /* FunctionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunctionsTests.swift; sourceTree = ""; }; DDB8126123F7B1C90079FEB5 /* ComplexArithmetic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplexArithmetic.swift; sourceTree = ""; }; DDB8126623F7B7F40079FEB5 /* ComplexArithmeticTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComplexArithmeticTests.swift; sourceTree = ""; }; + DDB8126B23FA2B950079FEB5 /* gyb.py */ = {isa = PBXFileReference; lastKnownFileType = text.script.python; path = gyb.py; sourceTree = ""; }; + DDB8126C23FA2B950079FEB5 /* gyb */ = {isa = PBXFileReference; lastKnownFileType = text; path = gyb; sourceTree = ""; }; + DDB8126F23FA2C210079FEB5 /* Functions.swift.gyb */ = {isa = PBXFileReference; explicitFileType = text.script.python; path = Functions.swift.gyb; sourceTree = ""; }; + DDB8127123FA4DA50079FEB5 /* ATTRIBUTIONS */ = {isa = PBXFileReference; lastKnownFileType = text; path = ATTRIBUTIONS; sourceTree = ""; }; + DDB8128523FB1A430079FEB5 /* FunctionsTests.swift.gyb */ = {isa = PBXFileReference; explicitFileType = text.script.python; path = FunctionsTests.swift.gyb; sourceTree = ""; }; DDFEEC3323EF13900096015C /* Complex.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Complex.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DDFEEC3723EF13900096015C /* Complex-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "Complex-Info.plist"; path = "Info/Complex-Info.plist"; sourceTree = ""; }; DDFEEC3C23EF13910096015C /* ComplexTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ComplexTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -207,6 +251,15 @@ name = Frameworks; sourceTree = ""; }; + DDB8126A23FA2B810079FEB5 /* Utils */ = { + isa = PBXGroup; + children = ( + DDB8126C23FA2B950079FEB5 /* gyb */, + DDB8126B23FA2B950079FEB5 /* gyb.py */, + ); + path = Utils; + sourceTree = ""; + }; DDFEEC2923EF13900096015C = { isa = PBXGroup; children = ( @@ -236,6 +289,7 @@ DDFEEC8E23F0B9F90096015C /* Sources */, DDFEECAA23F1BA280096015C /* Supporting Files */, DD94FBF723F3C50E0041D4EC /* Tests */, + DDB8126A23FA2B810079FEB5 /* Utils */, ); name = Complex; sourceTree = ""; @@ -246,6 +300,7 @@ DDB8121023F5AEB10079FEB5 /* ComplexTests.swift */, DDB8126623F7B7F40079FEB5 /* ComplexArithmeticTests.swift */, DDB8122023F656AC0079FEB5 /* FunctionsTests.swift */, + DDB8128523FB1A430079FEB5 /* FunctionsTests.swift.gyb */, DDFEEC4323EF13910096015C /* ComplexTests-Info.plist */, ); path = ComplexTests; @@ -268,6 +323,7 @@ DDFEECAE23F1BA5E0096015C /* .swiftlint.yml */, DDFEECAD23F1BA5E0096015C /* .travis.yml */, DDFEECAF23F1BA680096015C /* README.md */, + DDB8127123FA4DA50079FEB5 /* ATTRIBUTIONS */, DDFEECB123F1BA730096015C /* LICENSE */, DDFEEC3723EF13900096015C /* Complex-Info.plist */, ); @@ -279,6 +335,7 @@ children = ( DDB8120623F59B760079FEB5 /* Complex.swift */, DDB8126123F7B1C90079FEB5 /* ComplexArithmetic.swift */, + DDB8126F23FA2C210079FEB5 /* Functions.swift.gyb */, DDB8120B23F5A8E80079FEB5 /* Functions.swift */, ); path = Complex; @@ -330,6 +387,7 @@ buildRules = ( ); dependencies = ( + DDB8127823FA5C380079FEB5 /* PBXTargetDependency */, DDFEECB323F1BB5B0096015C /* PBXTargetDependency */, ); name = Complex; @@ -367,6 +425,7 @@ buildRules = ( ); dependencies = ( + DDB8127A23FA5C3E0079FEB5 /* PBXTargetDependency */, DDB8127C23FA5C3E0079FEB5 /* PBXTargetDependency */, ); name = "Complex macOS"; @@ -404,6 +463,7 @@ buildRules = ( ); dependencies = ( + DDB8127E23FA5C430079FEB5 /* PBXTargetDependency */, DDB8128023FA5C430079FEB5 /* PBXTargetDependency */, ); name = "Complex tvOS"; @@ -441,6 +501,7 @@ buildRules = ( ); dependencies = ( + DDB8128223FA5C480079FEB5 /* PBXTargetDependency */, DDB8128423FA5C480079FEB5 /* PBXTargetDependency */, ); name = "Complex watchOS"; @@ -458,6 +519,9 @@ LastUpgradeCheck = 1130; ORGANIZATIONNAME = SomeRandomiOSDev; TargetAttributes = { + DDB8127223FA5C0C0079FEB5 = { + CreatedOnToolsVersion = 11.3.1; + }; DDFEEC3223EF13900096015C = { CreatedOnToolsVersion = 11.3.1; LastSwiftMigration = 1130; @@ -511,6 +575,7 @@ DDFEECD923F2003E0096015C /* Complex tvOS */, DDFEECE123F2003E0096015C /* Complex tvOS Tests */, DDFEECF523F2004D0096015C /* Complex watchOS */, + DDB8127223FA5C0C0079FEB5 /* Generate Sources */, DDFEECA423F1BA0B0096015C /* Run SwiftLint */, ); }; @@ -569,6 +634,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + DDB8127623FA5C160079FEB5 /* Generate Sources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(SRCROOT)/Sources/Complex/Functions.swift.gyb", + "$(SRCROOT)/Tests/ComplexTests/FunctionsTests.swift.gyb", + ); + name = "Generate Sources"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/Sources/Complex/Functions.swift", + "$(SRCROOT)/Tests/ComplexTests/FunctionsTests.swift", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "./utils/gyb --line-directive '' -o ./Sources/Complex/Functions.swift ./Sources/Complex/Functions.swift.gyb\n./utils/gyb --line-directive '' -o ./Tests/ComplexTests/FunctionsTests.swift ./Tests/ComplexTests/FunctionsTests.swift.gyb\n"; + }; DDFEECA923F1BA160096015C /* Run SwiftLint */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -663,16 +750,36 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + DDB8127823FA5C380079FEB5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DDB8127223FA5C0C0079FEB5 /* Generate Sources */; + targetProxy = DDB8127723FA5C380079FEB5 /* PBXContainerItemProxy */; + }; + DDB8127A23FA5C3E0079FEB5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DDB8127223FA5C0C0079FEB5 /* Generate Sources */; + targetProxy = DDB8127923FA5C3E0079FEB5 /* PBXContainerItemProxy */; + }; DDB8127C23FA5C3E0079FEB5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DDFEECA423F1BA0B0096015C /* Run SwiftLint */; targetProxy = DDB8127B23FA5C3E0079FEB5 /* PBXContainerItemProxy */; }; + DDB8127E23FA5C430079FEB5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DDB8127223FA5C0C0079FEB5 /* Generate Sources */; + targetProxy = DDB8127D23FA5C430079FEB5 /* PBXContainerItemProxy */; + }; DDB8128023FA5C430079FEB5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DDFEECA423F1BA0B0096015C /* Run SwiftLint */; targetProxy = DDB8127F23FA5C430079FEB5 /* PBXContainerItemProxy */; }; + DDB8128223FA5C480079FEB5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DDB8127223FA5C0C0079FEB5 /* Generate Sources */; + targetProxy = DDB8128123FA5C480079FEB5 /* PBXContainerItemProxy */; + }; DDB8128423FA5C480079FEB5 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = DDFEECA423F1BA0B0096015C /* Run SwiftLint */; @@ -701,6 +808,22 @@ /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ + DDB8127423FA5C0C0079FEB5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + DDB8127523FA5C0C0079FEB5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; DDFEEC4523EF13910096015C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1199,6 +1322,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + DDB8127323FA5C0C0079FEB5 /* Build configuration list for PBXAggregateTarget "Generate Sources" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DDB8127423FA5C0C0079FEB5 /* Debug */, + DDB8127523FA5C0C0079FEB5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; DDFEEC2D23EF13900096015C /* Build configuration list for PBXProject "Complex" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/Sources/Complex/Functions.swift b/Sources/Complex/Functions.swift index 6aada0d..3836f34 100644 --- a/Sources/Complex/Functions.swift +++ b/Sources/Complex/Functions.swift @@ -61,3 +61,239 @@ public func clamp(_ value: Complex, _ minimum: F, _ maximum: F) -> Complex public func clamp(_ value: Complex, _ minimum: Complex, _ maximum: Complex) -> Complex where F: Comparable { return Complex(real: max(minimum.real, min(value.real, maximum.real)), imaginary: max(minimum.imaginary, min(value.imaginary, maximum.imaginary))) } + +// +// exp(a + bi) = exp(a) * (cos(b) + i * sin(b)) + +@_transparent +public func exp(_ value: Complex) -> Complex { + let exp = Darwin.exp(value.real) + return Complex(real: exp * cos(value.imaginary), imaginary: exp * sin(value.imaginary)) +} + +@_transparent +public func exp(_ value: Complex) -> Complex { + let exp = Darwin.exp(value.real) + return Complex(real: exp * cos(value.imaginary), imaginary: exp * sin(value.imaginary)) +} + +@_transparent +public func exp(_ value: Complex) -> Complex { + let exp = Darwin.exp(value.real) + return Complex(real: exp * cos(value.imaginary), imaginary: exp * sin(value.imaginary)) +} + +// +// log(a + bi) = log(sqrt(a^2 + b^2)) + i * atan(b / a) + +@_transparent +public func log(_ value: Complex) -> Complex { + return Complex(real: log(value.modulus), imaginary: value.angle) +} + +@_transparent +public func log(_ value: Complex) -> Complex { + return Complex(real: log(value.modulus), imaginary: value.angle) +} + +@_transparent +public func log(_ value: Complex) -> Complex { + return Complex(real: log(value.modulus), imaginary: value.angle) +} + +// + +@_transparent +public func log10(_ value: Complex) -> Complex { + return log(value) / log(10.0) +} + +@_transparent +public func log10(_ value: Complex) -> Complex { + return log(value) / log(10.0) +} + +@_transparent +public func log10(_ value: Complex) -> Complex { + return log(value) / log(10.0) +} + +// + +@_transparent +public func log2(_ value: Complex) -> Complex { + return log(value) / log(2.0) +} + +@_transparent +public func log2(_ value: Complex) -> Complex { + return log(value) / log(2.0) +} + +@_transparent +public func log2(_ value: Complex) -> Complex { + return log(value) / log(2.0) +} + +// +// sin(a + bi) = sin(a) * cosh(b) + i * cos(a) * sinh(b) + +@_transparent +public func sin(_ value: Complex) -> Complex { + return Complex(real: sin(value.real) * cosh(value.imaginary), imaginary: cos(value.real) * sinh(value.imaginary)) +} + +@_transparent +public func sin(_ value: Complex) -> Complex { + return Complex(real: sin(value.real) * cosh(value.imaginary), imaginary: cos(value.real) * sinh(value.imaginary)) +} + +@_transparent +public func sin(_ value: Complex) -> Complex { + return Complex(real: sin(value.real) * cosh(value.imaginary), imaginary: cos(value.real) * sinh(value.imaginary)) +} + +// +// asin(z) = -i * ln(iz + sqrt(1 - z^2)) + +@_transparent +public func asin(_ value: Complex) -> Complex { + return -.i * log(.i * value + sqrt(1.0 - value * value)) +} + +@_transparent +public func asin(_ value: Complex) -> Complex { + return -.i * log(.i * value + sqrt(1.0 - value * value)) +} + +@_transparent +public func asin(_ value: Complex) -> Complex { + return -.i * log(.i * value + sqrt(1.0 - value * value)) +} + +// +// sinh(z) = -i * sin(iz) + +@_transparent +public func sinh(_ value: Complex) -> Complex { + return -.i * sin(.i * value) +} + +@_transparent +public func sinh(_ value: Complex) -> Complex { + return -.i * sin(.i * value) +} + +@_transparent +public func sinh(_ value: Complex) -> Complex { + return -.i * sin(.i * value) +} + +// +// cos(a + bi) = cos(a) * cosh(b) - i * sin(a) * sinh(b) + +@_transparent +public func cos(_ value: Complex) -> Complex { + return Complex(real: cos(value.real) * cosh(value.imaginary), imaginary: -sin(value.real) * sinh(value.imaginary)) +} + +@_transparent +public func cos(_ value: Complex) -> Complex { + return Complex(real: cos(value.real) * cosh(value.imaginary), imaginary: -sin(value.real) * sinh(value.imaginary)) +} + +@_transparent +public func cos(_ value: Complex) -> Complex { + return Complex(real: cos(value.real) * cosh(value.imaginary), imaginary: -sin(value.real) * sinh(value.imaginary)) +} + +// +// acos(z) = -i * ln(z + sqrt(z^2 - 1)) + +@_transparent +public func acos(_ value: Complex) -> Complex { + return -.i * log(value + sqrt(value * value - 1.0)) +} + +@_transparent +public func acos(_ value: Complex) -> Complex { + return -.i * log(value + sqrt(value * value - 1.0)) +} + +@_transparent +public func acos(_ value: Complex) -> Complex { + return -.i * log(value + sqrt(value * value - 1.0)) +} + +// +// cosh(z) = cos(iz) + +@_transparent +public func cosh(_ value: Complex) -> Complex { + return cos(.i * value) +} + +@_transparent +public func cosh(_ value: Complex) -> Complex { + return cos(.i * value) +} + +@_transparent +public func cosh(_ value: Complex) -> Complex { + return cos(.i * value) +} + +// + +@_transparent +public func tan(_ value: Complex) -> Complex { + return sin(value) / cos(value) +} + +@_transparent +public func tan(_ value: Complex) -> Complex { + return sin(value) / cos(value) +} + +@_transparent +public func tan(_ value: Complex) -> Complex { + return sin(value) / cos(value) +} + +// +// atan(z) = (i / 2) * ln((i + z) / (i - z)) + +@_transparent +public func atan(_ value: Complex) -> Complex { + return .i * 0.5 * log((.i + value) / (.i - value)) +} + +@_transparent +public func atan(_ value: Complex) -> Complex { + return .i * 0.5 * log((.i + value) / (.i - value)) +} + +@_transparent +public func atan(_ value: Complex) -> Complex { + return .i * 0.5 * log((.i + value) / (.i - value)) +} + +// +// tanh(z) = -i * tan(iz) + +@_transparent +public func tanh(_ value: Complex) -> Complex { + return -.i * tan(.i * value) +} + +@_transparent +public func tanh(_ value: Complex) -> Complex { + return -.i * tan(.i * value) +} + +@_transparent +public func tanh(_ value: Complex) -> Complex { + return -.i * tan(.i * value) +} + +// diff --git a/Sources/Complex/Functions.swift.gyb b/Sources/Complex/Functions.swift.gyb new file mode 100644 index 0000000..3d1b759 --- /dev/null +++ b/Sources/Complex/Functions.swift.gyb @@ -0,0 +1,193 @@ +// +// Functions.swift +// Complex +// +// Copyright © 2020 SomeRandomiOSDev. All rights reserved. +// + +import Foundation + +@_transparent +public func csqrt(_ value: F) -> Complex where F: FloatingPoint { + let complex: Complex + if value < 0 { + complex = Complex(real: .zero, imaginary: sqrt(-value)) + } else { + complex = Complex(real: sqrt(value), imaginary: .zero) + } + + return complex +} + +@_transparent +public func sqrt(_ value: Complex) -> Complex where F: FloatingPoint { + //swiftlint:disable opening_brace statement_position + let sign = { (value: F) -> F in + let sign: F + if value < .zero { sign = -1 } + else { sign = 1 } + return sign + } + //swiftlint:enable opening_brace statement_position + + let modulus = value.modulus + let real = sqrt((value.real + modulus) / F(2)) + let imaginary = sign(value.imaginary) * sqrt((modulus - value.real) / F(2)) + + return Complex(real: real, imaginary: imaginary) +} + +@_transparent +public func abs(_ value: Complex) -> Complex where F: Comparable, F: SignedNumeric { + return Complex(real: abs(value.real), imaginary: abs(value.imaginary)) +} + +@_transparent +public func min(_ lhs: Complex, _ rhs: Complex) -> Complex where F: Comparable { + return Complex(real: min(lhs.real, rhs.real), imaginary: min(lhs.imaginary, rhs.imaginary)) +} + +@_transparent +public func max(_ lhs: Complex, _ rhs: Complex) -> Complex where F: Comparable { + return Complex(real: max(lhs.real, rhs.real), imaginary: max(lhs.imaginary, rhs.imaginary)) +} + +@_transparent +public func clamp(_ value: Complex, _ minimum: F, _ maximum: F) -> Complex where F: Comparable { + return Complex(real: max(minimum, min(value.real, maximum)), imaginary: max(minimum, min(value.imaginary, maximum))) +} + +@_transparent +public func clamp(_ value: Complex, _ minimum: Complex, _ maximum: Complex) -> Complex where F: Comparable { + return Complex(real: max(minimum.real, min(value.real, maximum.real)), imaginary: max(minimum.imaginary, min(value.imaginary, maximum.imaginary))) +} + +// +// exp(a + bi) = exp(a) * (cos(b) + i * sin(b)) + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func exp(_ value: Complex<${type}>) -> Complex<${type}> { + let exp = Darwin.exp(value.real) + return Complex<${type}>(real: exp * cos(value.imaginary), imaginary: exp * sin(value.imaginary)) +} + +% end +// +// log(a + bi) = log(sqrt(a^2 + b^2)) + i * atan(b / a) + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func log(_ value: Complex<${type}>) -> Complex<${type}> { + return Complex<${type}>(real: log(value.modulus), imaginary: value.angle) +} + +% end +// + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func log10(_ value: Complex<${type}>) -> Complex<${type}> { + return log(value) / log(10.0) +} + +% end +// + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func log2(_ value: Complex<${type}>) -> Complex<${type}> { + return log(value) / log(2.0) +} + +% end +// +// sin(a + bi) = sin(a) * cosh(b) + i * cos(a) * sinh(b) + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func sin(_ value: Complex<${type}>) -> Complex<${type}> { + return Complex<${type}>(real: sin(value.real) * cosh(value.imaginary), imaginary: cos(value.real) * sinh(value.imaginary)) +} + +% end +// +// asin(z) = -i * ln(iz + sqrt(1 - z^2)) + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func asin(_ value: Complex<${type}>) -> Complex<${type}> { + return -.i * log(.i * value + sqrt(1.0 - value * value)) +} + +% end +// +// sinh(z) = -i * sin(iz) + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func sinh(_ value: Complex<${type}>) -> Complex<${type}> { + return -.i * sin(.i * value) +} + +% end +// +// cos(a + bi) = cos(a) * cosh(b) - i * sin(a) * sinh(b) + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func cos(_ value: Complex<${type}>) -> Complex<${type}> { + return Complex<${type}>(real: cos(value.real) * cosh(value.imaginary), imaginary: -sin(value.real) * sinh(value.imaginary)) +} + +% end +// +// acos(z) = -i * ln(z + sqrt(z^2 - 1)) + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func acos(_ value: Complex<${type}>) -> Complex<${type}> { + return -.i * log(value + sqrt(value * value - 1.0)) +} + +% end +// +// cosh(z) = cos(iz) + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func cosh(_ value: Complex<${type}>) -> Complex<${type}> { + return cos(.i * value) +} + +% end +// + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func tan(_ value: Complex<${type}>) -> Complex<${type}> { + return sin(value) / cos(value) +} + +% end +// +// atan(z) = (i / 2) * ln((i + z) / (i - z)) + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func atan(_ value: Complex<${type}>) -> Complex<${type}> { + return .i * 0.5 * log((.i + value) / (.i - value)) +} + +% end +// +// tanh(z) = -i * tan(iz) + +% for type in ["Float", "Double", "Float80"]: +@_transparent +public func tanh(_ value: Complex<${type}>) -> Complex<${type}> { + return -.i * tan(.i * value) +} + +% end +// diff --git a/Tests/ComplexTests/FunctionsTests.swift b/Tests/ComplexTests/FunctionsTests.swift index 8749a02..b882cd1 100644 --- a/Tests/ComplexTests/FunctionsTests.swift +++ b/Tests/ComplexTests/FunctionsTests.swift @@ -90,4 +90,177 @@ class FunctionsTests: XCTestCase { XCTAssertEqual(clamp(Complex(real: -4.5, imaginary: 3.7), -4.0, 4.0), Complex(real: -4.0, imaginary: 3.7)) XCTAssertEqual(clamp(Complex(real: -4.5, imaginary: 3.7), Complex(real: 0.0, imaginary: 0.0), Complex(real: 2.0, imaginary: 4.0)), Complex(real: 0.0, imaginary: 3.7)) } + + func test_exp() { + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: 0.0)), Complex(real: exp(2.0), imaginary: 0.0), accuracy: 0.0001) + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: .pi * 0.5)), Complex(real: 0.0, imaginary: exp(2.0)), accuracy: 0.0001) + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: .pi)), Complex(real: -exp(2.0), imaginary: 0.0), accuracy: 0.0001) + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: .pi * 1.5)), Complex(real: 0.0, imaginary: -exp(2.0)), accuracy: 0.0001) + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: 0.0)), Complex(real: exp(2.0), imaginary: 0.0), accuracy: 0.0001) + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: .pi * 0.5)), Complex(real: 0.0, imaginary: exp(2.0)), accuracy: 0.0001) + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: .pi)), Complex(real: -exp(2.0), imaginary: 0.0), accuracy: 0.0001) + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: .pi * 1.5)), Complex(real: 0.0, imaginary: -exp(2.0)), accuracy: 0.0001) + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: 0.0)), Complex(real: exp(2.0), imaginary: 0.0), accuracy: 0.0001) + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: .pi * 0.5)), Complex(real: 0.0, imaginary: exp(2.0)), accuracy: 0.0001) + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: .pi)), Complex(real: -exp(2.0), imaginary: 0.0), accuracy: 0.0001) + XCTAssertEqual(exp(Complex(real: 2.0, imaginary: .pi * 1.5)), Complex(real: 0.0, imaginary: -exp(2.0)), accuracy: 0.0001) + } + + func test_log() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(log(complex), Complex(real: log(complex.modulus), imaginary: complex.angle), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(log(complex), Complex(real: log(complex.modulus), imaginary: complex.angle), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(log(complex), Complex(real: log(complex.modulus), imaginary: complex.angle), accuracy: 0.0001) + } + } + + func test_log10() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(log10(complex), log(complex) / log(10), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(log10(complex), log(complex) / log(10), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(log10(complex), log(complex) / log(10), accuracy: 0.0001) + } + } + + func test_log2() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(log2(complex), log(complex) / log(2), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(log2(complex), log(complex) / log(2), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(log2(complex), log(complex) / log(2), accuracy: 0.0001) + } + } + + func test_sin() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(sin(complex), Complex(real: sin(complex.real) * cosh(complex.imaginary), imaginary: cos(complex.real) * sinh(complex.imaginary)), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(sin(complex), Complex(real: sin(complex.real) * cosh(complex.imaginary), imaginary: cos(complex.real) * sinh(complex.imaginary)), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(sin(complex), Complex(real: sin(complex.real) * cosh(complex.imaginary), imaginary: cos(complex.real) * sinh(complex.imaginary)), accuracy: 0.0001) + } + } + + func test_asin() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(asin(complex), -.i * log(.i * complex + sqrt(1.0 - complex * complex)), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(asin(complex), -.i * log(.i * complex + sqrt(1.0 - complex * complex)), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(asin(complex), -.i * log(.i * complex + sqrt(1.0 - complex * complex)), accuracy: 0.0001) + } + } + + func test_sinh() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(sinh(complex), -.i * sin(.i * complex), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(sinh(complex), -.i * sin(.i * complex), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(sinh(complex), -.i * sin(.i * complex), accuracy: 0.0001) + } + } + + func test_cos() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(cos(complex), Complex(real: cos(complex.real) * cosh(complex.imaginary), imaginary: -sin(complex.real) * sinh(complex.imaginary)), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(cos(complex), Complex(real: cos(complex.real) * cosh(complex.imaginary), imaginary: -sin(complex.real) * sinh(complex.imaginary)), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(cos(complex), Complex(real: cos(complex.real) * cosh(complex.imaginary), imaginary: -sin(complex.real) * sinh(complex.imaginary)), accuracy: 0.0001) + } + } + + func test_acos() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(acos(complex), -.i * log(complex + sqrt(complex * complex - 1.0)), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(acos(complex), -.i * log(complex + sqrt(complex * complex - 1.0)), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(acos(complex), -.i * log(complex + sqrt(complex * complex - 1.0)), accuracy: 0.0001) + } + } + + func test_cosh() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(cosh(complex), cos(.i * complex), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(cosh(complex), cos(.i * complex), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(cosh(complex), cos(.i * complex), accuracy: 0.0001) + } + } + + func test_tan() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(tan(complex), sin(complex) / cos(complex), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(tan(complex), sin(complex) / cos(complex), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(tan(complex), sin(complex) / cos(complex), accuracy: 0.0001) + } + } + + func test_atan() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(atan(complex), .i * 0.5 * log((.i + complex) / (.i - complex)), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(atan(complex), .i * 0.5 * log((.i + complex) / (.i - complex)), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(atan(complex), .i * 0.5 * log((.i + complex) / (.i - complex)), accuracy: 0.0001) + } + } + + func test_tanh() { + for complex in sampleComplexNumbers(ofType: Float.self) { + XCTAssertEqual(tanh(complex), -.i * tan(.i * complex), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Double.self) { + XCTAssertEqual(tanh(complex), -.i * tan(.i * complex), accuracy: 0.0001) + } + for complex in sampleComplexNumbers(ofType: Float80.self) { + XCTAssertEqual(tanh(complex), -.i * tan(.i * complex), accuracy: 0.0001) + } + } + + // MARK: Private Methods + + private func sampleComplexNumbers(ofType: Scalar.Type) -> [Complex] where Scalar: BinaryFloatingPoint { + return [Complex(real: 2.0, imaginary: 0.0), + Complex(real: 2.0, imaginary: .pi * 0.5), + Complex(real: 2.0, imaginary: .pi), + Complex(real: 2.0, imaginary: .pi * 1.5)] + } +} + +func XCTAssertEqual(_ expression1: @autoclosure () throws -> Complex, _ expression2: @autoclosure () throws -> Complex, accuracy: T, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) rethrows where T: FloatingPoint { + let difference = try abs(expression1() - expression2()) + XCTAssertTrue(difference.real <= accuracy && difference.imaginary <= accuracy, message(), file: file, line: line) } diff --git a/Tests/ComplexTests/FunctionsTests.swift.gyb b/Tests/ComplexTests/FunctionsTests.swift.gyb new file mode 100644 index 0000000..f7eff13 --- /dev/null +++ b/Tests/ComplexTests/FunctionsTests.swift.gyb @@ -0,0 +1,212 @@ +// +// FunctionsTests.swift +// Complex +// +// Copyright © 2020 SomeRandomiOSDev. All rights reserved. +// + +@testable import Complex +import Half +import XCTest + +class FunctionsTests: XCTestCase { + + // MARK: Test Methods + + func test_csqrt() { + XCTAssertEqual(csqrt(Half(-4.0)), Complex(real: 0.0, imaginary: 2.0)) + XCTAssertEqual(csqrt(Half(9.0)), Complex(real: 3.0, imaginary: 0.0)) + + XCTAssertEqual(csqrt(Float(-4.0)), Complex(real: 0.0, imaginary: 2.0)) + XCTAssertEqual(csqrt(Float(9.0)), Complex(real: 3.0, imaginary: 0.0)) + + XCTAssertEqual(csqrt(Double(-4.0)), Complex(real: 0.0, imaginary: 2.0)) + XCTAssertEqual(csqrt(Double(9.0)), Complex(real: 3.0, imaginary: 0.0)) + + XCTAssertEqual(csqrt(Float80(-4.0)), Complex(real: 0.0, imaginary: 2.0)) + XCTAssertEqual(csqrt(Float80(9.0)), Complex(real: 3.0, imaginary: 0.0)) + } + + func test_sqrt() { + XCTAssertEqual(sqrt(Complex(real: -4.0, imaginary: 0.0)), Complex(real: 0.0, imaginary: 2.0)) + XCTAssertEqual(sqrt(Complex(real: 4.0, imaginary: 0.0)), Complex(real: 2.0, imaginary: 0.0)) + XCTAssertEqual(sqrt(Complex(real: 0.0, imaginary: -4.0)), Complex(real: 2.0.squareRoot(), imaginary: -2.0.squareRoot())) + XCTAssertEqual(sqrt(Complex(real: 0.0, imaginary: 4.0)), Complex(real: 2.0.squareRoot(), imaginary: 2.0.squareRoot())) + XCTAssertEqual(sqrt(Complex(real: 3.0, imaginary: 4.0)), Complex(real: 2.0, imaginary: 1.0)) + XCTAssertEqual(sqrt(Complex(real: -3.0, imaginary: 4.0)), Complex(real: 1.0, imaginary: 2.0)) + + XCTAssertEqual(sqrt(Complex(real: -4.0, imaginary: 0.0)), Complex(real: 0.0, imaginary: 2.0)) + XCTAssertEqual(sqrt(Complex(real: 4.0, imaginary: 0.0)), Complex(real: 2.0, imaginary: 0.0)) + XCTAssertEqual(sqrt(Complex(real: 0.0, imaginary: -4.0)), Complex(real: 2.0.squareRoot(), imaginary: -2.0.squareRoot())) + XCTAssertEqual(sqrt(Complex(real: 0.0, imaginary: 4.0)), Complex(real: 2.0.squareRoot(), imaginary: 2.0.squareRoot())) + XCTAssertEqual(sqrt(Complex(real: 3.0, imaginary: 4.0)), Complex(real: 2.0, imaginary: 1.0)) + XCTAssertEqual(sqrt(Complex(real: -3.0, imaginary: 4.0)), Complex(real: 1.0, imaginary: 2.0)) + + XCTAssertEqual(sqrt(Complex(real: -4.0, imaginary: 0.0)), Complex(real: 0.0, imaginary: 2.0)) + XCTAssertEqual(sqrt(Complex(real: 4.0, imaginary: 0.0)), Complex(real: 2.0, imaginary: 0.0)) + XCTAssertEqual(sqrt(Complex(real: 0.0, imaginary: -4.0)), Complex(real: 2.0.squareRoot(), imaginary: -2.0.squareRoot())) + XCTAssertEqual(sqrt(Complex(real: 0.0, imaginary: 4.0)), Complex(real: 2.0.squareRoot(), imaginary: 2.0.squareRoot())) + XCTAssertEqual(sqrt(Complex(real: 3.0, imaginary: 4.0)), Complex(real: 2.0, imaginary: 1.0)) + XCTAssertEqual(sqrt(Complex(real: -3.0, imaginary: 4.0)), Complex(real: 1.0, imaginary: 2.0)) + + XCTAssertEqual(sqrt(Complex(real: -4.0, imaginary: 0.0)), Complex(real: 0.0, imaginary: 2.0)) + XCTAssertEqual(sqrt(Complex(real: 4.0, imaginary: 0.0)), Complex(real: 2.0, imaginary: 0.0)) + XCTAssertTrue((sqrt(Complex(real: 0.0, imaginary: -4.0)) - Complex(real: 2.0.squareRoot(), imaginary: -2.0.squareRoot())).modulus < 0.0001) + XCTAssertTrue((sqrt(Complex(real: 0.0, imaginary: 4.0)) - Complex(real: 2.0.squareRoot(), imaginary: 2.0.squareRoot())).modulus < 0.0001) + XCTAssertEqual(sqrt(Complex(real: 3.0, imaginary: 4.0)), Complex(real: 2.0, imaginary: 1.0)) + XCTAssertEqual(sqrt(Complex(real: -3.0, imaginary: 4.0)), Complex(real: 1.0, imaginary: 2.0)) + } + + func test_abs() { + XCTAssertEqual(abs(Complex(real: -4.5, imaginary: 3.7)), Complex(real: 4.5, imaginary: 3.7)) + XCTAssertEqual(abs(Complex(real: -4.5, imaginary: 3.7)), Complex(real: 4.5, imaginary: 3.7)) + XCTAssertEqual(abs(Complex(real: -4.5, imaginary: 3.7)), Complex(real: 4.5, imaginary: 3.7)) + XCTAssertEqual(abs(Complex(real: -4.5, imaginary: 3.7)), Complex(real: 4.5, imaginary: 3.7)) + } + + func test_min() { + XCTAssertEqual(min(Complex(real: -4.5, imaginary: 3.7), Complex(real: 7.0, imaginary: 1.2)), Complex(real: -4.5, imaginary: 1.2)) + XCTAssertEqual(min(Complex(real: -4.5, imaginary: 3.7), Complex(real: 7.0, imaginary: 1.2)), Complex(real: -4.5, imaginary: 1.2)) + XCTAssertEqual(min(Complex(real: -4.5, imaginary: 3.7), Complex(real: 7.0, imaginary: 1.2)), Complex(real: -4.5, imaginary: 1.2)) + XCTAssertEqual(min(Complex(real: -4.5, imaginary: 3.7), Complex(real: 7.0, imaginary: 1.2)), Complex(real: -4.5, imaginary: 1.2)) + } + + func test_max() { + XCTAssertEqual(max(Complex(real: -4.5, imaginary: 3.7), Complex(real: 7.0, imaginary: 1.2)), Complex(real: 7.0, imaginary: 3.7)) + XCTAssertEqual(max(Complex(real: -4.5, imaginary: 3.7), Complex(real: 7.0, imaginary: 1.2)), Complex(real: 7.0, imaginary: 3.7)) + XCTAssertEqual(max(Complex(real: -4.5, imaginary: 3.7), Complex(real: 7.0, imaginary: 1.2)), Complex(real: 7.0, imaginary: 3.7)) + XCTAssertEqual(max(Complex(real: -4.5, imaginary: 3.7), Complex(real: 7.0, imaginary: 1.2)), Complex(real: 7.0, imaginary: 3.7)) + } + + func test_clamp() { + XCTAssertEqual(clamp(Complex(real: -4.5, imaginary: 3.7), -4.0, 4.0), Complex(real: -4.0, imaginary: 3.7)) + XCTAssertEqual(clamp(Complex(real: -4.5, imaginary: 3.7), Complex(real: 0.0, imaginary: 0.0), Complex(real: 2.0, imaginary: 4.0)), Complex(real: 0.0, imaginary: 3.7)) + + XCTAssertEqual(clamp(Complex(real: -4.5, imaginary: 3.7), -4.0, 4.0), Complex(real: -4.0, imaginary: 3.7)) + XCTAssertEqual(clamp(Complex(real: -4.5, imaginary: 3.7), Complex(real: 0.0, imaginary: 0.0), Complex(real: 2.0, imaginary: 4.0)), Complex(real: 0.0, imaginary: 3.7)) + + XCTAssertEqual(clamp(Complex(real: -4.5, imaginary: 3.7), -4.0, 4.0), Complex(real: -4.0, imaginary: 3.7)) + XCTAssertEqual(clamp(Complex(real: -4.5, imaginary: 3.7), Complex(real: 0.0, imaginary: 0.0), Complex(real: 2.0, imaginary: 4.0)), Complex(real: 0.0, imaginary: 3.7)) + XCTAssertEqual(clamp(Complex(real: -4.5, imaginary: 3.7), -4.0, 4.0), Complex(real: -4.0, imaginary: 3.7)) + XCTAssertEqual(clamp(Complex(real: -4.5, imaginary: 3.7), Complex(real: 0.0, imaginary: 0.0), Complex(real: 2.0, imaginary: 4.0)), Complex(real: 0.0, imaginary: 3.7)) + } + + func test_exp() { + % for type in ["Float", "Double", "Float80"]: + XCTAssertEqual(exp(Complex<${type}>(real: 2.0, imaginary: 0.0)), Complex<${type}>(real: exp(2.0), imaginary: 0.0), accuracy: 0.0001) + XCTAssertEqual(exp(Complex<${type}>(real: 2.0, imaginary: .pi * 0.5)), Complex<${type}>(real: 0.0, imaginary: exp(2.0)), accuracy: 0.0001) + XCTAssertEqual(exp(Complex<${type}>(real: 2.0, imaginary: .pi)), Complex<${type}>(real: -exp(2.0), imaginary: 0.0), accuracy: 0.0001) + XCTAssertEqual(exp(Complex<${type}>(real: 2.0, imaginary: .pi * 1.5)), Complex<${type}>(real: 0.0, imaginary: -exp(2.0)), accuracy: 0.0001) + % end + } + + func test_log() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(log(complex), Complex<${type}>(real: log(complex.modulus), imaginary: complex.angle), accuracy: 0.0001) + } + % end + } + + func test_log10() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(log10(complex), log(complex) / log(10), accuracy: 0.0001) + } + % end + } + + func test_log2() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(log2(complex), log(complex) / log(2), accuracy: 0.0001) + } + % end + } + + func test_sin() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(sin(complex), Complex<${type}>(real: sin(complex.real) * cosh(complex.imaginary), imaginary: cos(complex.real) * sinh(complex.imaginary)), accuracy: 0.0001) + } + % end + } + + func test_asin() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(asin(complex), -.i * log(.i * complex + sqrt(1.0 - complex * complex)), accuracy: 0.0001) + } + % end + } + + func test_sinh() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(sinh(complex), -.i * sin(.i * complex), accuracy: 0.0001) + } + % end + } + + func test_cos() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(cos(complex), Complex<${type}>(real: cos(complex.real) * cosh(complex.imaginary), imaginary: -sin(complex.real) * sinh(complex.imaginary)), accuracy: 0.0001) + } + % end + } + + func test_acos() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(acos(complex), -.i * log(complex + sqrt(complex * complex - 1.0)), accuracy: 0.0001) + } + % end + } + + func test_cosh() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(cosh(complex), cos(.i * complex), accuracy: 0.0001) + } + % end + } + + func test_tan() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(tan(complex), sin(complex) / cos(complex), accuracy: 0.0001) + } + % end + } + + func test_atan() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(atan(complex), .i * 0.5 * log((.i + complex) / (.i - complex)), accuracy: 0.0001) + } + % end + } + + func test_tanh() { + % for type in ["Float", "Double", "Float80"]: + for complex in sampleComplexNumbers(ofType: ${type}.self) { + XCTAssertEqual(tanh(complex), -.i * tan(.i * complex), accuracy: 0.0001) + } + % end + } + + // MARK: Private Methods + + private func sampleComplexNumbers(ofType: Scalar.Type) -> [Complex] where Scalar: BinaryFloatingPoint { + return [Complex(real: 2.0, imaginary: 0.0), + Complex(real: 2.0, imaginary: .pi * 0.5), + Complex(real: 2.0, imaginary: .pi), + Complex(real: 2.0, imaginary: .pi * 1.5)] + } +} + +func XCTAssertEqual(_ expression1: @autoclosure () throws -> Complex, _ expression2: @autoclosure () throws -> Complex, accuracy: T, _ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) rethrows where T: FloatingPoint { + let difference = try abs(expression1() - expression2()) + XCTAssertTrue(difference.real <= accuracy && difference.imaginary <= accuracy, message(), file: file, line: line) +} diff --git a/utils/gyb b/utils/gyb new file mode 100755 index 0000000..dece788 --- /dev/null +++ b/utils/gyb @@ -0,0 +1,3 @@ +#!/usr/bin/env python2.7 +import gyb +gyb.main() diff --git a/utils/gyb.py b/utils/gyb.py new file mode 100644 index 0000000..2df9a03 --- /dev/null +++ b/utils/gyb.py @@ -0,0 +1,1260 @@ +#!/usr/bin/env python +# GYB: Generate Your Boilerplate (improved names welcome; at least +# this one's short). See -h output for instructions + +from __future__ import print_function + +import os +import re +import sys +import textwrap +import tokenize +from bisect import bisect + + +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO + + +try: + basestring +except NameError: + basestring = str + + +def get_line_starts(s): + """Return a list containing the start index of each line in s. + + The list also contains a sentinel index for the end of the string, + so there will be one more element in the list than there are lines + in the string + """ + starts = [0] + + for line in s.split('\n'): + starts.append(starts[-1] + len(line) + 1) + + starts[-1] -= 1 + return starts + + +def strip_trailing_nl(s): + """If s ends with a newline, drop it; else return s intact""" + return s[:-1] if s.endswith('\n') else s + + +def split_lines(s): + """Split s into a list of lines, each of which has a trailing newline + + If the lines are later concatenated, the result is s, possibly + with a single appended newline. + """ + return [l + '\n' for l in s.split('\n')] + + +# text on a line up to the first '$$', '${', or '%%' +literalText = r'(?: [^$\n%] | \$(?![${]) | %(?!%) )*' + +# The part of an '%end' line that follows the '%' sign +linesClose = r'[\ \t]* end [\ \t]* (?: \# .* )? $' + +# Note: Where "# Absorb" appears below, the regexp attempts to eat up +# through the end of ${...} and %{...}% constructs. In reality we +# handle this with the Python tokenizer, which avoids mis-detections +# due to nesting, comments and strings. This extra absorption in the +# regexp facilitates testing the regexp on its own, by preventing the +# interior of some of these constructs from being treated as literal +# text. +tokenize_re = re.compile( + r''' +# %-lines and %{...}-blocks + # \n? # absorb one preceding newline + ^ + (?: + (?P + (?P<_indent> [\ \t]* % (?! [{%] ) [\ \t]* ) (?! [\ \t] | ''' + + linesClose + r''' ) .* + ( \n (?P=_indent) (?! ''' + linesClose + r''' ) .* ) * + ) + | (?P [\ \t]* % [ \t]* ''' + linesClose + r''' ) + | [\ \t]* (?P %\{ ) + (?: [^}]| \} (?!%) )* \}% # Absorb + ) + \n? # absorb one trailing newline + +# Substitutions +| (?P \$\{ ) + [^}]* \} # Absorb + +# %% and $$ are literal % and $ respectively +| (?P[$%]) (?P=symbol) + +# Literal text +| (?P ''' + literalText + r''' + (?: + # newline that doesn't precede space+% + (?: \n (?! [\ \t]* %[^%] ) ) + ''' + literalText + r''' + )* + \n? + ) +''', re.VERBOSE | re.MULTILINE) + +gyb_block_close = re.compile(r'\}%[ \t]*\n?') + + +def token_pos_to_index(token_pos, start, line_starts): + """Translate a tokenize (line, column) pair into an absolute + position in source text given the position where we started + tokenizing and a list that maps lines onto their starting + character indexes. + """ + relative_token_line_plus1, token_col = token_pos + + # line number where we started tokenizing + start_line_num = bisect(line_starts, start) - 1 + + # line number of the token in the whole text + abs_token_line = relative_token_line_plus1 - 1 + start_line_num + + # if found in the first line, adjust the end column to account + # for the extra text + if relative_token_line_plus1 == 1: + token_col += start - line_starts[start_line_num] + + # Sometimes tokenizer errors report a line beyond the last one + if abs_token_line >= len(line_starts): + return line_starts[-1] + + return line_starts[abs_token_line] + token_col + + +def tokenize_python_to_unmatched_close_curly(source_text, start, line_starts): + """Apply Python's tokenize to source_text starting at index start + while matching open and close curly braces. When an unmatched + close curly brace is found, return its index. If not found, + return len(source_text). If there's a tokenization error, return + the position of the error. + """ + stream = StringIO(source_text) + stream.seek(start) + nesting = 0 + + try: + for kind, text, token_start, token_end, line_text \ + in tokenize.generate_tokens(stream.readline): + + if text == '{': + nesting += 1 + elif text == '}': + nesting -= 1 + if nesting < 0: + return token_pos_to_index(token_start, start, line_starts) + + except tokenize.TokenError as error: + (message, error_pos) = error.args + return token_pos_to_index(error_pos, start, line_starts) + + return len(source_text) + + +def tokenize_template(template_text): + r"""Given the text of a template, returns an iterator over + (tokenType, token, match) tuples. + + **Note**: this is template syntax tokenization, not Python + tokenization. + + When a non-literal token is matched, a client may call + iter.send(pos) on the iterator to reset the position in + template_text at which scanning will resume. + + This function provides a base level of tokenization which is + then refined by ParseContext.token_generator. + + >>> from pprint import * + >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( + ... '%for x in range(10):\n% print x\n%end\njuicebox'))) + [('gybLines', '%for x in range(10):\n% print x'), + ('gybLinesClose', '%end'), + ('literal', 'juicebox')] + + >>> pprint(list((kind, text) for kind, text, _ in tokenize_template( + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... '''))) + [('literal', 'Nothing\n'), + ('gybLines', '% if x:\n% for i in range(3):'), + ('substitutionOpen', '${'), + ('literal', '\n'), + ('gybLinesClose', '% end'), + ('gybLines', '% else:'), + ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n')] + + >>> for kind, text, _ in tokenize_template(''' + ... This is $some$ literal stuff containing a ${substitution} + ... followed by a %{...} block: + ... %{ + ... # Python code + ... }% + ... and here $${are} some %-lines: + ... % x = 1 + ... % y = 2 + ... % if z == 3: + ... % print '${hello}' + ... % end + ... % for x in zz: + ... % print x + ... % # different indentation + ... % twice + ... and some lines that literally start with a %% token + ... %% first line + ... %% second line + ... '''): + ... print((kind, text.strip().split('\n',1)[0])) + ('literal', 'This is $some$ literal stuff containing a') + ('substitutionOpen', '${') + ('literal', 'followed by a %{...} block:') + ('gybBlockOpen', '%{') + ('literal', 'and here ${are} some %-lines:') + ('gybLines', '% x = 1') + ('gybLinesClose', '% end') + ('gybLines', '% for x in zz:') + ('gybLines', '% # different indentation') + ('gybLines', '% twice') + ('literal', 'and some lines that literally start with a % token') + """ + pos = 0 + end = len(template_text) + + saved_literal = [] + literal_first_match = None + + while pos < end: + m = tokenize_re.match(template_text, pos, end) + + # pull out the one matched key (ignoring internal patterns starting + # with _) + ((kind, text), ) = ( + (kind, text) for (kind, text) in m.groupdict().items() + if text is not None and kind[0] != '_') + + if kind in ('literal', 'symbol'): + if len(saved_literal) == 0: + literal_first_match = m + # literals and symbols get batched together + saved_literal.append(text) + pos = None + else: + # found a non-literal. First yield any literal we've accumulated + if saved_literal != []: + yield 'literal', ''.join(saved_literal), literal_first_match + saved_literal = [] + + # Then yield the thing we found. If we get a reply, it's + # the place to resume tokenizing + pos = yield kind, text, m + + # If we were not sent a new position by our client, resume + # tokenizing at the end of this match. + if pos is None: + pos = m.end(0) + else: + # Client is not yet ready to process next token + yield + + if saved_literal != []: + yield 'literal', ''.join(saved_literal), literal_first_match + + +def split_gyb_lines(source_lines): + r"""Return a list of lines at which to split the incoming source + + These positions represent the beginnings of python line groups that + will require a matching %end construct if they are to be closed. + + >>> src = split_lines('''\ + ... if x: + ... print x + ... if y: # trailing comment + ... print z + ... if z: # another comment\ + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 2 + >>> src[s[0]] + ' print z\n' + >>> s[1] - len(src) + 0 + + >>> src = split_lines('''\ + ... if x: + ... if y: print 1 + ... if z: + ... print 2 + ... pass\ + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 1 + >>> src[s[0]] + ' if y: print 1\n' + + >>> src = split_lines('''\ + ... if x: + ... if y: + ... print 1 + ... print 2 + ... ''') + >>> s = split_gyb_lines(src) + >>> len(s) + 2 + >>> src[s[0]] + ' if y:\n' + >>> src[s[1]] + ' print 1\n' + """ + last_token_text, last_token_kind = None, None + unmatched_indents = [] + + dedents = 0 + try: + for token_kind, token_text, token_start, \ + (token_end_line, token_end_col), line_text \ + in tokenize.generate_tokens(lambda i=iter(source_lines): + next(i)): + + if token_kind in (tokenize.COMMENT, tokenize.ENDMARKER): + continue + + if token_text == '\n' and last_token_text == ':': + unmatched_indents.append(token_end_line) + + # The tokenizer appends dedents at EOF; don't consider + # those as matching indentations. Instead just save them + # up... + if last_token_kind == tokenize.DEDENT: + dedents += 1 + # And count them later, when we see something real. + if token_kind != tokenize.DEDENT and dedents > 0: + unmatched_indents = unmatched_indents[:-dedents] + dedents = 0 + + last_token_text, last_token_kind = token_text, token_kind + + except tokenize.TokenError: + # Let the later compile() call report the error + return [] + + if last_token_text == ':': + unmatched_indents.append(len(source_lines)) + + return unmatched_indents + + +def code_starts_with_dedent_keyword(source_lines): + r"""Return True iff the incoming Python source_lines begin with "else", + "elif", "except", or "finally". + + Initial comments and whitespace are ignored. + + >>> code_starts_with_dedent_keyword(split_lines('if x in y: pass')) + False + >>> code_starts_with_dedent_keyword(split_lines('except ifSomethingElse:')) + True + >>> code_starts_with_dedent_keyword( + ... split_lines('\n# comment\nelse: # yes')) + True + """ + token_text = None + for token_kind, token_text, _, _, _ \ + in tokenize.generate_tokens(lambda i=iter(source_lines): next(i)): + + if token_kind != tokenize.COMMENT and token_text.strip() != '': + break + + return token_text in ('else', 'elif', 'except', 'finally') + + +class ParseContext(object): + + """State carried through a parse of a template""" + + filename = '' + template = '' + line_starts = [] + code_start_line = -1 + code_text = None + tokens = None # The rest of the tokens + close_lines = False + + def __init__(self, filename, template=None): + self.filename = os.path.abspath(filename) + if sys.platform == 'win32': + self.filename = self.filename.replace('\\', '/') + if template is None: + with open(filename) as f: + self.template = f.read() + else: + self.template = template + self.line_starts = get_line_starts(self.template) + self.tokens = self.token_generator(tokenize_template(self.template)) + self.next_token() + + def pos_to_line(self, pos): + return bisect(self.line_starts, pos) - 1 + + def token_generator(self, base_tokens): + r"""Given an iterator over (kind, text, match) triples (see + tokenize_template above), return a refined iterator over + token_kinds. + + Among other adjustments to the elements found by base_tokens, + this refined iterator tokenizes python code embedded in + template text to help determine its true extent. The + expression "base_tokens.send(pos)" is used to reset the index at + which base_tokens resumes scanning the underlying text. + + >>> ctx = ParseContext('dummy', ''' + ... %for x in y: + ... % print x + ... % end + ... literally + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('literal', '\n') + ('gybLinesOpen', 'for x in y:\n') + ('gybLines', ' print x\n') + ('gybLinesClose', '% end') + ('literal', 'literally\n') + + >>> ctx = ParseContext('dummy', + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('literal', 'Nothing\n') + ('gybLinesOpen', 'if x:\n') + ('gybLinesOpen', ' for i in range(3):\n') + ('substitutionOpen', 'i') + ('literal', '\n') + ('gybLinesClose', '% end') + ('gybLinesOpen', 'else:\n') + ('literal', 'THIS SHOULD NOT APPEAR IN THE OUTPUT\n') + + >>> ctx = ParseContext('dummy', + ... '''% for x in [1, 2, 3]: + ... % if x == 1: + ... literal1 + ... % elif x > 1: # add output line here to fix bug + ... % if x == 2: + ... literal2 + ... % end + ... % end + ... % end + ... ''') + >>> while ctx.token_kind: + ... print((ctx.token_kind, ctx.code_text or ctx.token_text)) + ... ignored = ctx.next_token() + ('gybLinesOpen', 'for x in [1, 2, 3]:\n') + ('gybLinesOpen', ' if x == 1:\n') + ('literal', 'literal1\n') + ('gybLinesOpen', 'elif x > 1: # add output line here to fix bug\n') + ('gybLinesOpen', ' if x == 2:\n') + ('literal', 'literal2\n') + ('gybLinesClose', '% end') + ('gybLinesClose', '% end') + ('gybLinesClose', '% end') + """ + for self.token_kind, self.token_text, self.token_match in base_tokens: + kind = self.token_kind + self.code_text = None + + # Do we need to close the current lines? + self.close_lines = kind == 'gybLinesClose' + + # %{...}% and ${...} constructs + if kind.endswith('Open'): + + # Tokenize text that follows as Python up to an unmatched '}' + code_start = self.token_match.end(kind) + self.code_start_line = self.pos_to_line(code_start) + + close_pos = tokenize_python_to_unmatched_close_curly( + self.template, code_start, self.line_starts) + self.code_text = self.template[code_start:close_pos] + yield kind + + if (kind == 'gybBlockOpen'): + # Absorb any '}% \n' + m2 = gyb_block_close.match(self.template, close_pos) + if not m2: + raise ValueError("Invalid block closure") + next_pos = m2.end(0) + else: + assert kind == 'substitutionOpen' + # skip past the closing '}' + next_pos = close_pos + 1 + + # Resume tokenizing after the end of the code. + base_tokens.send(next_pos) + + elif kind == 'gybLines': + + self.code_start_line = self.pos_to_line( + self.token_match.start('gybLines')) + indentation = self.token_match.group('_indent') + + # Strip off the leading indentation and %-sign + source_lines = re.split( + '^' + re.escape(indentation), + self.token_match.group('gybLines') + '\n', + flags=re.MULTILINE)[1:] + + if code_starts_with_dedent_keyword(source_lines): + self.close_lines = True + + last_split = 0 + for line in split_gyb_lines(source_lines): + self.token_kind = 'gybLinesOpen' + self.code_text = ''.join(source_lines[last_split:line]) + yield self.token_kind + last_split = line + self.code_start_line += line - last_split + self.close_lines = False + + self.code_text = ''.join(source_lines[last_split:]) + if self.code_text: + self.token_kind = 'gybLines' + yield self.token_kind + else: + yield self.token_kind + + def next_token(self): + """Move to the next token""" + for kind in self.tokens: + return self.token_kind + + self.token_kind = None + + +_default_line_directive = \ + '// ###sourceLocation(file: "%(file)s", line: %(line)d)' + + +class ExecutionContext(object): + + """State we pass around during execution of a template""" + + def __init__(self, line_directive=_default_line_directive, + **local_bindings): + self.local_bindings = local_bindings + self.line_directive = line_directive + self.local_bindings['__context__'] = self + self.result_text = [] + self.last_file_line = None + + def append_text(self, text, file, line): + # see if we need to inject a line marker + if self.line_directive: + if (file, line) != self.last_file_line: + # We can only insert the line directive at a line break + if len(self.result_text) == 0 \ + or self.result_text[-1].endswith('\n'): + substitutions = {'file': file, 'line': line + 1} + format_str = self.line_directive + '\n' + self.result_text.append(format_str % substitutions) + # But if the new text contains any line breaks, we can create + # one + elif '\n' in text: + i = text.find('\n') + self.result_text.append(text[:i + 1]) + # and try again + self.append_text(text[i + 1:], file, line + 1) + return + + self.result_text.append(text) + self.last_file_line = (file, line + text.count('\n')) + + +class ASTNode(object): + + """Abstract base class for template AST nodes""" + + def __init__(self): + raise NotImplementedError("ASTNode.__init__ is not implemented.") + + def execute(self, context): + raise NotImplementedError("ASTNode.execute is not implemented.") + + def __str__(self, indent=''): + raise NotImplementedError("ASTNode.__str__ is not implemented.") + + def format_children(self, indent): + if not self.children: + return ' []' + + return '\n'.join( + ['', indent + '['] + + [x.__str__(indent + 4 * ' ') for x in self.children] + + [indent + ']']) + + +class Block(ASTNode): + + """A sequence of other AST nodes, to be executed in order""" + + children = [] + + def __init__(self, context): + self.children = [] + + while context.token_kind and not context.close_lines: + if context.token_kind == 'literal': + node = Literal + else: + node = Code + self.children.append(node(context)) + + def execute(self, context): + for x in self.children: + x.execute(context) + + def __str__(self, indent=''): + return indent + 'Block:' + self.format_children(indent) + + +class Literal(ASTNode): + + """An AST node that generates literal text""" + + def __init__(self, context): + self.text = context.token_text + start_position = context.token_match.start(context.token_kind) + self.start_line_number = context.pos_to_line(start_position) + self.filename = context.filename + context.next_token() + + def execute(self, context): + context.append_text(self.text, self.filename, self.start_line_number) + + def __str__(self, indent=''): + return '\n'.join( + [indent + x for x in ['Literal:'] + + strip_trailing_nl(self.text).split('\n')]) + + +class Code(ASTNode): + + """An AST node that is evaluated as Python""" + + code = None + children = () + kind = None + + def __init__(self, context): + + source = '' + source_line_count = 0 + + def accumulate_code(): + s = source + (context.code_start_line - source_line_count) * '\n' \ + + textwrap.dedent(context.code_text) + line_count = context.code_start_line + \ + context.code_text.count('\n') + context.next_token() + return s, line_count + + eval_exec = 'exec' + if context.token_kind.startswith('substitution'): + eval_exec = 'eval' + source, source_line_count = accumulate_code() + source = '(' + source.strip() + ')' + + else: + while context.token_kind == 'gybLinesOpen': + source, source_line_count = accumulate_code() + source += ' __children__[%d].execute(__context__)\n' % len( + self.children) + source_line_count += 1 + + self.children += (Block(context),) + + if context.token_kind == 'gybLinesClose': + context.next_token() + + if context.token_kind == 'gybLines': + source, source_line_count = accumulate_code() + + # Only handle a substitution as part of this code block if + # we don't already have some %-lines. + elif context.token_kind == 'gybBlockOpen': + + # Opening ${...} and %{...}% constructs + source, source_line_count = accumulate_code() + + self.filename = context.filename + self.start_line_number = context.code_start_line + self.code = compile(source, context.filename, eval_exec) + self.source = source + + def execute(self, context): + # Save __children__ from the local bindings + save_children = context.local_bindings.get('__children__') + # Execute the code with our __children__ in scope + context.local_bindings['__children__'] = self.children + context.local_bindings['__file__'] = self.filename + result = eval(self.code, context.local_bindings) + + if context.local_bindings['__children__'] is not self.children: + raise ValueError("The code is not allowed to mutate __children__") + # Restore the bindings + context.local_bindings['__children__'] = save_children + + # If we got a result, the code was an expression, so append + # its value + if result is not None \ + or (isinstance(result, basestring) and result != ''): + from numbers import Number, Integral + result_string = None + if isinstance(result, Number) and not isinstance(result, Integral): + result_string = repr(result) + else: + result_string = str(result) + context.append_text( + result_string, self.filename, self.start_line_number) + + def __str__(self, indent=''): + source_lines = re.sub(r'^\n', '', strip_trailing_nl( + self.source), flags=re.MULTILINE).split('\n') + if len(source_lines) == 1: + s = indent + 'Code: {' + source_lines[0] + '}' + else: + s = indent + 'Code:\n' + indent + '{\n' + '\n'.join( + indent + 4 * ' ' + l for l in source_lines + ) + '\n' + indent + '}' + return s + self.format_children(indent) + + +def expand(filename, line_directive=_default_line_directive, **local_bindings): + r"""Return the contents of the given template file, executed with the given + local bindings. + + >>> from tempfile import NamedTemporaryFile + >>> # On Windows, the name of a NamedTemporaryFile cannot be used to open + >>> # the file for a second time if delete=True. Therefore, we have to + >>> # manually handle closing and deleting this file to allow us to open + >>> # the file by its name across all platforms. + >>> f = NamedTemporaryFile(delete=False) + >>> f.write( + ... r'''--- + ... % for i in range(int(x)): + ... a pox on ${i} for epoxy + ... % end + ... ${120 + + ... + ... 3} + ... abc + ... ${"w\nx\nX\ny"} + ... z + ... ''') + >>> f.flush() + >>> result = expand( + ... f.name, + ... line_directive='//#sourceLocation(file: "%(file)s", ' + \ + ... 'line: %(line)d)', + ... x=2 + ... ).replace( + ... '"%s"' % f.name.replace('\\', '/'), '"dummy.file"') + >>> print(result, end='') + //#sourceLocation(file: "dummy.file", line: 1) + --- + //#sourceLocation(file: "dummy.file", line: 3) + a pox on 0 for epoxy + //#sourceLocation(file: "dummy.file", line: 3) + a pox on 1 for epoxy + //#sourceLocation(file: "dummy.file", line: 5) + 123 + //#sourceLocation(file: "dummy.file", line: 8) + abc + w + x + X + y + //#sourceLocation(file: "dummy.file", line: 10) + z + >>> f.close() + >>> os.remove(f.name) + """ + with open(filename) as f: + t = parse_template(filename, f.read()) + d = os.getcwd() + os.chdir(os.path.dirname(os.path.abspath(filename))) + try: + return execute_template( + t, line_directive=line_directive, **local_bindings) + finally: + os.chdir(d) + + +def parse_template(filename, text=None): + r"""Return an AST corresponding to the given template file. + + If text is supplied, it is assumed to be the contents of the file, + as a string. + + >>> print(parse_template('dummy.file', text= + ... '''% for x in [1, 2, 3]: + ... % if x == 1: + ... literal1 + ... % elif x > 1: # add output line after this line to fix bug + ... % if x == 2: + ... literal2 + ... % end + ... % end + ... % end + ... ''')) + Block: + [ + Code: + { + for x in [1, 2, 3]: + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: + { + if x == 1: + __children__[0].execute(__context__) + elif x > 1: # add output line after this line to fix bug + __children__[1].execute(__context__) + } + [ + Block: + [ + Literal: + literal1 + ] + Block: + [ + Code: + { + if x == 2: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + literal2 + ] + ] + ] + ] + ] + ] + ] + + >>> print(parse_template( + ... 'dummy.file', + ... text='%for x in range(10):\n% print(x)\n%end\njuicebox')) + Block: + [ + Code: + { + for x in range(10): + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {print(x)} [] + ] + ] + Literal: + juicebox + ] + + >>> print(parse_template('/dummy.file', text= + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''')) + Block: + [ + Literal: + Nothing + Code: + { + if x: + __children__[0].execute(__context__) + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: + { + for i in range(3): + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {(i)} [] + Literal: + + ] + ] + ] + Block: + [ + Literal: + THIS SHOULD NOT APPEAR IN THE OUTPUT + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %for x in y: + ... % print(y) + ... ''')) + Block: + [ + Code: + { + for x in y: + __children__[0].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %if x: + ... % print(y) + ... AAAA + ... %else: + ... BBBB + ... ''')) + Block: + [ + Code: + { + if x: + __children__[0].execute(__context__) + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + Literal: + AAAA + ] + Block: + [ + Literal: + BBBB + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''% + ... %if x: + ... % print(y) + ... AAAA + ... %# This is a comment + ... %else: + ... BBBB + ... ''')) + Block: + [ + Code: + { + if x: + __children__[0].execute(__context__) + # This is a comment + else: + __children__[1].execute(__context__) + } + [ + Block: + [ + Code: {print(y)} [] + Literal: + AAAA + ] + Block: + [ + Literal: + BBBB + ] + ] + ] + + >>> print(parse_template('dummy.file', text='''\ + ... %for x in y: + ... AAAA + ... %if x: + ... BBBB + ... %end + ... CCCC + ... ''')) + Block: + [ + Code: + { + for x in y: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + AAAA + Code: + { + if x: + __children__[0].execute(__context__) + } + [ + Block: + [ + Literal: + BBBB + ] + ] + Literal: + CCCC + ] + ] + ] + """ + return Block(ParseContext(filename, text)) + + +def execute_template( + ast, line_directive=_default_line_directive, **local_bindings): + r"""Return the text generated by executing the given template AST. + + Keyword arguments become local variable bindings in the execution context + + >>> root_directory = os.path.abspath('/') + >>> file_name = (root_directory + 'dummy.file').replace('\\', '/') + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % if x: + ... % for i in range(3): + ... ${i} + ... % end + ... % else: + ... THIS SHOULD NOT APPEAR IN THE OUTPUT + ... ''') + >>> out = execute_template(ast, + ... line_directive='//#sourceLocation(file: "%(file)s", line: %(line)d)', + ... x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + //#sourceLocation(file: "DUMMY-FILE", line: 1) + Nothing + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 0 + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 1 + //#sourceLocation(file: "DUMMY-FILE", line: 4) + 2 + + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % a = [] + ... % for x in range(3): + ... % a.append(x) + ... % end + ... ${a} + ... ''') + >>> out = execute_template(ast, + ... line_directive='//#sourceLocation(file: "%(file)s", line: %(line)d)', + ... x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + //#sourceLocation(file: "DUMMY-FILE", line: 1) + Nothing + //#sourceLocation(file: "DUMMY-FILE", line: 6) + [0, 1, 2] + + >>> ast = parse_template(file_name, text= + ... '''Nothing + ... % a = [] + ... % for x in range(3): + ... % a.append(x) + ... % end + ... ${a} + ... ''') + >>> out = execute_template(ast, + ... line_directive='#line %(line)d "%(file)s"', x=1) + >>> out = out.replace(file_name, "DUMMY-FILE") + >>> print(out, end="") + #line 1 "DUMMY-FILE" + Nothing + #line 6 "DUMMY-FILE" + [0, 1, 2] + """ + execution_context = ExecutionContext( + line_directive=line_directive, **local_bindings) + ast.execute(execution_context) + return ''.join(execution_context.result_text) + + +def main(): + import argparse + import sys + + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description='Generate Your Boilerplate!', epilog=''' + A GYB template consists of the following elements: + + - Literal text which is inserted directly into the output + + - %% or $$ in literal text, which insert literal '%' and '$' + symbols respectively. + + - Substitutions of the form ${}. The Python + expression is converted to a string and the result is inserted + into the output. + + - Python code delimited by %{...}%. Typically used to inject + definitions (functions, classes, variable bindings) into the + evaluation context of the template. Common indentation is + stripped, so you can add as much indentation to the beginning + of this code as you like + + - Lines beginning with optional whitespace followed by a single + '%' and Python code. %-lines allow you to nest other + constructs inside them. To close a level of nesting, use the + "%end" construct. + + - Lines beginning with optional whitespace and followed by a + single '%' and the token "end", which close open constructs in + %-lines. + + Example template: + + - Hello - + %{ + x = 42 + def succ(a): + return a+1 + }% + + I can assure you that ${x} < ${succ(x)} + + % if int(y) > 7: + % for i in range(3): + y is greater than seven! + % end + % else: + y is less than or equal to seven + % end + + - The End. - + + When run with "gyb -Dy=9", the output is + + - Hello - + + I can assure you that 42 < 43 + + y is greater than seven! + y is greater than seven! + y is greater than seven! + + - The End. - +''' + ) + parser.add_argument( + '-D', action='append', dest='defines', metavar='NAME=VALUE', + default=[], + help='''Bindings to be set in the template's execution context''') + + parser.add_argument( + 'file', type=argparse.FileType(), + help='Path to GYB template file (defaults to stdin)', nargs='?', + default=sys.stdin) + parser.add_argument( + '-o', dest='target', type=argparse.FileType('w'), + help='Output file (defaults to stdout)', default=sys.stdout) + parser.add_argument( + '--test', action='store_true', + default=False, help='Run a self-test') + parser.add_argument( + '--verbose-test', action='store_true', + default=False, help='Run a verbose self-test') + parser.add_argument( + '--dump', action='store_true', + default=False, help='Dump the parsed template to stdout') + parser.add_argument( + '--line-directive', + default=_default_line_directive, + help=''' + Line directive format string, which will be + provided 2 substitutions, `%%(line)d` and `%%(file)s`. + + Example: `#sourceLocation(file: "%%(file)s", line: %%(line)d)` + + The default works automatically with the `line-directive` tool, + which see for more information. + ''') + + args = parser.parse_args(sys.argv[1:]) + + if args.test or args.verbose_test: + import doctest + selfmod = sys.modules[__name__] + if doctest.testmod(selfmod, verbose=args.verbose_test or None).failed: + sys.exit(1) + + bindings = dict(x.split('=', 1) for x in args.defines) + ast = parse_template(args.file.name, args.file.read()) + if args.dump: + print(ast) + # Allow the template to open files and import .py files relative to its own + # directory + os.chdir(os.path.dirname(os.path.abspath(args.file.name))) + sys.path = ['.'] + sys.path + + args.target.write(execute_template(ast, args.line_directive, **bindings)) + + +if __name__ == '__main__': + main()