فهرست منبع

feat: add DynBound, clamp, double equality helpers

Sam Jaffe 2 سال پیش
والد
کامیت
1a528e4ea2
8فایلهای تغییر یافته به همراه667 افزوده شده و 68 حذف شده
  1. 18 0
      include/math/clamp.h
  2. 36 0
      include/math/double_equals.h
  3. 68 0
      include/math/dyn_limit.h
  4. 28 21
      include/math/limit.h
  5. 408 0
      limit.xcodeproj/project.pbxproj
  6. 36 0
      test/dyn_limit_test.cxx
  7. 31 47
      test/limit_test.cxx
  8. 42 0
      test/xcode_gtest_helper.h

+ 18 - 0
include/math/clamp.h

@@ -0,0 +1,18 @@
+//
+//  clamp.h
+//  limit
+//
+//  Created by Sam Jaffe on 12/9/23.
+//
+
+#pragma once
+
+#include <algorithm>
+#include <utility>
+
+namespace math {
+template <typename T>
+T const & clamp(T const & value, T const & min, T const & max) {
+  return std::max(min, std::min(value, max));
+}
+}

+ 36 - 0
include/math/double_equals.h

@@ -0,0 +1,36 @@
+//
+//  double_equals.h
+//  limit
+//
+//  Created by Sam Jaffe on 12/9/23.
+//
+
+#pragma once
+
+#include <algorithm>
+
+namespace math {
+inline bool approx_equal(double lhs, double rhs, double eps) {
+  double const a = std::abs(lhs);
+  double const b = std::abs(rhs);
+  return std::abs(lhs - rhs) <= (std::max(a, b) * eps);
+}
+
+inline bool essentially_equal(double lhs, double rhs, double eps) {
+  double const a = std::abs(lhs);
+  double const b = std::abs(rhs);
+  return std::abs(lhs - rhs) <= (std::min(a, b) * eps);
+}
+
+inline bool definitely_greater(double lhs, double rhs, double eps) {
+  double const a = std::abs(lhs);
+  double const b = std::abs(rhs);
+  return (lhs - rhs) > (std::max(a, b) * eps);
+}
+
+inline bool definitely_less(double lhs, double rhs, double eps) {
+  double const a = std::abs(lhs);
+  double const b = std::abs(rhs);
+  return (rhs - lhs) > (std::max(a, b) * eps);
+}
+}

+ 68 - 0
include/math/dyn_limit.h

@@ -0,0 +1,68 @@
+//
+//  dyn_limit.h
+//  pokemon
+//
+//  Created by Sam Jaffe on 12/1/23.
+//  Copyright © 2023 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#include <math/clamp.h>
+
+namespace math {
+template <typename T, T, T> class Bound;
+
+template <typename T> class DynBound final {
+public:
+  using value_type = T;
+  using underlying_type = value_type;
+
+private:
+  value_type lower_bound_{};
+  value_type upper_bound_{};
+  value_type value_{};
+
+public:
+  DynBound() = default;
+  DynBound(value_type value, value_type lower_bound, value_type upper_bound)
+      : lower_bound_(lower_bound), upper_bound_(upper_bound),
+        value_(clamp(value, lower_bound_, upper_bound_)) {}
+
+  template <value_type MINIMUM_VALUE, value_type MAXIMUM_VALUE>
+  DynBound(Bound<value_type, MINIMUM_VALUE, MAXIMUM_VALUE> const & other)
+      : lower_bound_(MINIMUM_VALUE), upper_bound_(MAXIMUM_VALUE),
+        value_(other) {}
+
+  operator value_type() const { return value_; }
+  bool is_min() const { return value_ == lower_bound_; }
+  bool is_max() const { return value_ == upper_bound_; }
+
+  DynBound & operator+=(value_type by) {
+    value_ = std::min<value_type>(upper_bound_, value_ + by);
+    return *this;
+  }
+
+  DynBound & operator-=(value_type by) {
+    value_ = std::max<value_type>(lower_bound_, value_ - by);
+    return *this;
+  }
+
+  DynBound & operator++() { return operator+=(1); }
+  DynBound & operator--() { return operator-=(1); }
+  DynBound operator+(value_type by) const { return DynBound{*this} += by; }
+  DynBound operator-(value_type by) const { return DynBound{*this} -= by; }
+
+  DynBound operator++(int) {
+    DynBound tmp{*this};
+    operator++();
+    return tmp;
+  }
+
+  DynBound operator--(int) {
+    DynBound tmp{*this};
+    operator--();
+    return tmp;
+  }
+};
+}

+ 28 - 21
include/math/limit.h

@@ -10,58 +10,65 @@
 
 #include <stdexcept>
 
+#include <math/clamp.h>
+
 namespace math {
 
-struct {} assert_bounds;
+struct {
+} assert_bounds;
 using AssertBounds = decltype(assert_bounds);
 
-template <typename T, T MINIMUM_VALUE, T MAXIMUM_VALUE>
-class Bound final {
-private:
+template <typename T, T MINIMUM_VALUE, T MAXIMUM_VALUE> class Bound final {
 public:
   static_assert(MINIMUM_VALUE <= MAXIMUM_VALUE,
                 "The minimum value must be less than or equal to the maximum");
-  
-  typedef Tp value_type;
+
+  using value_type = T;
+  using underlying_type = value_type;
+
   static constexpr const value_type min = MINIMUM_VALUE;
   static constexpr const value_type max = MAXIMUM_VALUE;
-  
+
+private:
+  value_type value{min};
+
+public:
   constexpr Bound() = default;
   constexpr Bound(value_type const & val) noexcept
-      : value(std::max(min, std::min(max, val))) {}
-  
+      : value(clamp(val, min, max)) {}
+
   Bound(AssertBounds, value_type val) : value(val) {
     if (val < min || max < val) {
       throw std::out_of_range{"Must construct a value within range"};
     }
   }
-  
-  Bound& operator-=(value_type v) { return *this = Bound(value - v); }
-  Bound& operator+=(value_type v) { return *this = Bound(value + v); }
-  Bound& operator--() { return *this -= 1; }
-  Bound& operator++() { return *this += 1; }
-  
+
+  Bound & operator-=(value_type by) { return *this = Bound(value - by); }
+  Bound & operator+=(value_type by) { return *this = Bound(value + by); }
+  Bound operator+(value_type by) const { return Bound{*this} += by; }
+  Bound operator-(value_type by) const { return Bound{*this} -= by; }
+  Bound & operator--() { return *this -= 1; }
+  Bound & operator++() { return *this += 1; }
+
   Bound operator--(int) {
     Bound tmp = *this;
     operator--();
     return tmp;
   }
-  
+
   Bound operator++(int) {
     Bound tmp = *this;
     operator++();
     return tmp;
   }
-  
+
   operator value_type() const { return value; }
-  
-private:
-  value_type value{};
 };
 
 template <typename T, T MINMAX> using SymBound = Bound<T, -MINMAX, +MINMAX>;
 template <typename T, T VAL>
-using UniBound = std::conditional_t<0 <= VAL, Bound<T, 0, VAL>, Bound<T, VAL, 0>>;
+using UniBound =
+    std::conditional_t<0 <= VAL, Bound<T, 0, VAL>, Bound<T, VAL, 0>>;
 
 template <typename L, L l1, L h1, typename R, R l2, R h2>
 auto operator+(Bound<L, l1, h1> lhs, Bound<R, l2, h2> rhs) {

+ 408 - 0
limit.xcodeproj/project.pbxproj

@@ -6,30 +6,130 @@
 	objectVersion = 46;
 	objects = {
 
+/* Begin PBXBuildFile section */
+		CD46B2132B24A6E300591B3D /* math in Headers */ = {isa = PBXBuildFile; fileRef = CDDAA47C28655E6300D5EDD4 /* math */; settings = {ATTRIBUTES = (Public, ); }; };
+		CD46B2262B24A75500591B3D /* limit_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDFB745E1E451F61007D4841 /* limit_test.cxx */; };
+		CD46B2292B24A76100591B3D /* GoogleMock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD46B1F32B24A55E00591B3D /* GoogleMock.framework */; };
+		CD46B22B2B24A8B500591B3D /* dyn_limit_test.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD46B22A2B24A8B500591B3D /* dyn_limit_test.cxx */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+		CD46B1F22B24A55E00591B3D /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD46B1EB2B24A55E00591B3D /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05818F861A685AEA0072A469;
+			remoteInfo = GoogleMock;
+		};
+		CD46B1F42B24A55E00591B3D /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD46B1EB2B24A55E00591B3D /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05E96ABD1A68600C00204102;
+			remoteInfo = gmock;
+		};
+		CD46B1F62B24A55E00591B3D /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD46B1EB2B24A55E00591B3D /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05E96B1F1A68634900204102;
+			remoteInfo = gtest;
+		};
+		CD46B1F82B24A55E00591B3D /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD46B1EB2B24A55E00591B3D /* GoogleMock.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = 05818F901A685AEA0072A469;
+			remoteInfo = GoogleMockTests;
+		};
+		CD46B2212B24A73E00591B3D /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 0E5DFDC61BB4D3360063976E /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = CD46B20E2B24A6D900591B3D;
+			remoteInfo = limit;
+		};
+		CD46B2272B24A75C00591B3D /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CD46B1EB2B24A55E00591B3D /* GoogleMock.xcodeproj */;
+			proxyType = 1;
+			remoteGlobalIDString = 05818F851A685AEA0072A469;
+			remoteInfo = GoogleMock;
+		};
+/* End PBXContainerItemProxy section */
+
 /* Begin PBXFileReference section */
+		CD46B1EA2B24A54800591B3D /* dyn_limit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = dyn_limit.h; sourceTree = "<group>"; };
+		CD46B1EB2B24A55E00591B3D /* GoogleMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GoogleMock.xcodeproj; path = "../../../gmock-xcode-master/GoogleMock.xcodeproj"; sourceTree = "<group>"; };
+		CD46B1FA2B24A57500591B3D /* xcode_gtest_helper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = xcode_gtest_helper.h; sourceTree = "<group>"; };
+		CD46B20F2B24A6D900591B3D /* liblimit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = liblimit.a; sourceTree = BUILT_PRODUCTS_DIR; };
+		CD46B21C2B24A73E00591B3D /* limit-test.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "limit-test.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
+		CD46B22A2B24A8B500591B3D /* dyn_limit_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = dyn_limit_test.cxx; sourceTree = "<group>"; };
+		CD46B22C2B24AA0100591B3D /* clamp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = clamp.h; sourceTree = "<group>"; };
+		CD46B22D2B24AAB600591B3D /* double_equals.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = double_equals.h; sourceTree = "<group>"; };
 		CDDAA47C28655E6300D5EDD4 /* math */ = {isa = PBXFileReference; lastKnownFileType = folder; name = math; path = include/math; sourceTree = "<group>"; };
 		CDDAA47D28655E8F00D5EDD4 /* limit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = limit.h; sourceTree = "<group>"; };
 		CDFB745E1E451F61007D4841 /* limit_test.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = limit_test.cxx; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
+/* Begin PBXFrameworksBuildPhase section */
+		CD46B20D2B24A6D900591B3D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		CD46B2192B24A73E00591B3D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CD46B2292B24A76100591B3D /* GoogleMock.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
 /* Begin PBXGroup section */
 		0E5DFDC51BB4D3360063976E = {
 			isa = PBXGroup;
 			children = (
+				CD46B1EB2B24A55E00591B3D /* GoogleMock.xcodeproj */,
 				CDDAA47C28655E6300D5EDD4 /* math */,
 				CDDAA47928655E5C00D5EDD4 /* include */,
 				CDFB745C1E451F45007D4841 /* test */,
 				0E5DFE1C1BB4DB740063976E /* Products */,
+				CD46B2092B24A6BA00591B3D /* Frameworks */,
 			);
 			sourceTree = "<group>";
 		};
 		0E5DFE1C1BB4DB740063976E /* Products */ = {
 			isa = PBXGroup;
 			children = (
+				CD46B20F2B24A6D900591B3D /* liblimit.a */,
+				CD46B21C2B24A73E00591B3D /* limit-test.xctest */,
 			);
 			name = Products;
 			sourceTree = "<group>";
 		};
+		CD46B1EC2B24A55E00591B3D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				CD46B1F32B24A55E00591B3D /* GoogleMock.framework */,
+				CD46B1F52B24A55E00591B3D /* gmock.framework */,
+				CD46B1F72B24A55E00591B3D /* gtest.framework */,
+				CD46B1F92B24A55E00591B3D /* GoogleMockTests.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		CD46B2092B24A6BA00591B3D /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
 		CDDAA47928655E5C00D5EDD4 /* include */ = {
 			isa = PBXGroup;
 			children = (
@@ -41,6 +141,9 @@
 		CDDAA47A28655E5C00D5EDD4 /* math */ = {
 			isa = PBXGroup;
 			children = (
+				CD46B22C2B24AA0100591B3D /* clamp.h */,
+				CD46B22D2B24AAB600591B3D /* double_equals.h */,
+				CD46B1EA2B24A54800591B3D /* dyn_limit.h */,
 				CDDAA47D28655E8F00D5EDD4 /* limit.h */,
 			);
 			path = math;
@@ -49,6 +152,8 @@
 		CDFB745C1E451F45007D4841 /* test */ = {
 			isa = PBXGroup;
 			children = (
+				CD46B1FA2B24A57500591B3D /* xcode_gtest_helper.h */,
+				CD46B22A2B24A8B500591B3D /* dyn_limit_test.cxx */,
 				CDFB745E1E451F61007D4841 /* limit_test.cxx */,
 			);
 			path = test;
@@ -56,11 +161,72 @@
 		};
 /* End PBXGroup section */
 
+/* Begin PBXHeadersBuildPhase section */
+		CD46B20B2B24A6D900591B3D /* Headers */ = {
+			isa = PBXHeadersBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CD46B2132B24A6E300591B3D /* math in Headers */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXHeadersBuildPhase section */
+
+/* Begin PBXNativeTarget section */
+		CD46B20E2B24A6D900591B3D /* limit */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = CD46B2102B24A6D900591B3D /* Build configuration list for PBXNativeTarget "limit" */;
+			buildPhases = (
+				CD46B20B2B24A6D900591B3D /* Headers */,
+				CD46B20C2B24A6D900591B3D /* Sources */,
+				CD46B20D2B24A6D900591B3D /* Frameworks */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = limit;
+			productName = limit;
+			productReference = CD46B20F2B24A6D900591B3D /* liblimit.a */;
+			productType = "com.apple.product-type.library.static";
+		};
+		CD46B21B2B24A73E00591B3D /* limit-test */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = CD46B2232B24A73E00591B3D /* Build configuration list for PBXNativeTarget "limit-test" */;
+			buildPhases = (
+				CD46B2182B24A73E00591B3D /* Sources */,
+				CD46B2192B24A73E00591B3D /* Frameworks */,
+				CD46B21A2B24A73E00591B3D /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				CD46B2282B24A75C00591B3D /* PBXTargetDependency */,
+				CD46B2222B24A73E00591B3D /* PBXTargetDependency */,
+			);
+			name = "limit-test";
+			productName = "limit-test";
+			productReference = CD46B21C2B24A73E00591B3D /* limit-test.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
+/* End PBXNativeTarget section */
+
 /* Begin PBXProject section */
 		0E5DFDC61BB4D3360063976E /* Project object */ = {
 			isa = PBXProject;
 			attributes = {
+				LastSwiftUpdateCheck = 1340;
 				LastUpgradeCheck = 1230;
+				TargetAttributes = {
+					CD46B20E2B24A6D900591B3D = {
+						CreatedOnToolsVersion = 13.4.1;
+						ProvisioningStyle = Automatic;
+					};
+					CD46B21B2B24A73E00591B3D = {
+						CreatedOnToolsVersion = 13.4.1;
+						ProvisioningStyle = Automatic;
+					};
+				};
 			};
 			buildConfigurationList = 0E5DFDC91BB4D3360063976E /* Build configuration list for PBXProject "limit" */;
 			compatibilityVersion = "Xcode 3.2";
@@ -73,12 +239,93 @@
 			mainGroup = 0E5DFDC51BB4D3360063976E;
 			productRefGroup = 0E5DFE1C1BB4DB740063976E /* Products */;
 			projectDirPath = "";
+			projectReferences = (
+				{
+					ProductGroup = CD46B1EC2B24A55E00591B3D /* Products */;
+					ProjectRef = CD46B1EB2B24A55E00591B3D /* GoogleMock.xcodeproj */;
+				},
+			);
 			projectRoot = "";
 			targets = (
+				CD46B20E2B24A6D900591B3D /* limit */,
+				CD46B21B2B24A73E00591B3D /* limit-test */,
 			);
 		};
 /* End PBXProject section */
 
+/* Begin PBXReferenceProxy section */
+		CD46B1F32B24A55E00591B3D /* GoogleMock.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = GoogleMock.framework;
+			remoteRef = CD46B1F22B24A55E00591B3D /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		CD46B1F52B24A55E00591B3D /* gmock.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = gmock.framework;
+			remoteRef = CD46B1F42B24A55E00591B3D /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		CD46B1F72B24A55E00591B3D /* gtest.framework */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.framework;
+			path = gtest.framework;
+			remoteRef = CD46B1F62B24A55E00591B3D /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		CD46B1F92B24A55E00591B3D /* GoogleMockTests.xctest */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.cfbundle;
+			path = GoogleMockTests.xctest;
+			remoteRef = CD46B1F82B24A55E00591B3D /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+/* End PBXReferenceProxy section */
+
+/* Begin PBXResourcesBuildPhase section */
+		CD46B21A2B24A73E00591B3D /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		CD46B20C2B24A6D900591B3D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		CD46B2182B24A73E00591B3D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CD46B2262B24A75500591B3D /* limit_test.cxx in Sources */,
+				CD46B22B2B24A8B500591B3D /* dyn_limit_test.cxx in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+		CD46B2222B24A73E00591B3D /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = CD46B20E2B24A6D900591B3D /* limit */;
+			targetProxy = CD46B2212B24A73E00591B3D /* PBXContainerItemProxy */;
+		};
+		CD46B2282B24A75C00591B3D /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			name = GoogleMock;
+			targetProxy = CD46B2272B24A75C00591B3D /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
 /* Begin XCBuildConfiguration section */
 		0E5DFDC71BB4D3360063976E /* Debug */ = {
 			isa = XCBuildConfiguration;
@@ -114,6 +361,8 @@
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = macosx;
+				SYSTEM_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include";
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include";
 			};
 			name = Debug;
 		};
@@ -149,6 +398,147 @@
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				SDKROOT = macosx;
+				SYSTEM_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include";
+				USER_HEADER_SEARCH_PATHS = "$(PROJECT_DIR)/include";
+			};
+			name = Release;
+		};
+		CD46B2112B24A6D900591B3D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				EXECUTABLE_PREFIX = lib;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				MACOSX_DEPLOYMENT_TARGET = 12.3;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+			};
+			name = Debug;
+		};
+		CD46B2122B24A6D900591B3D /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				EXECUTABLE_PREFIX = lib;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				MACOSX_DEPLOYMENT_TARGET = 12.3;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SKIP_INSTALL = YES;
+			};
+			name = Release;
+		};
+		CD46B2242B24A73E00591B3D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GENERATE_INFOPLIST_FILE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 12.3;
+				MARKETING_VERSION = 1.0;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = "leumasjaffe.limit-test";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+			};
+			name = Debug;
+		};
+		CD46B2252B24A73E00591B3D /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CODE_SIGN_STYLE = Automatic;
+				COPY_PHASE_STRIP = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				GCC_C_LANGUAGE_STANDARD = gnu11;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GENERATE_INFOPLIST_FILE = YES;
+				MACOSX_DEPLOYMENT_TARGET = 12.3;
+				MARKETING_VERSION = 1.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				PRODUCT_BUNDLE_IDENTIFIER = "leumasjaffe.limit-test";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_EMIT_LOC_STRINGS = NO;
+				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+				SWIFT_VERSION = 5.0;
 			};
 			name = Release;
 		};
@@ -164,6 +554,24 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
+		CD46B2102B24A6D900591B3D /* Build configuration list for PBXNativeTarget "limit" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				CD46B2112B24A6D900591B3D /* Debug */,
+				CD46B2122B24A6D900591B3D /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		CD46B2232B24A73E00591B3D /* Build configuration list for PBXNativeTarget "limit-test" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				CD46B2242B24A73E00591B3D /* Debug */,
+				CD46B2252B24A73E00591B3D /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 /* End XCConfigurationList section */
 	};
 	rootObject = 0E5DFDC61BB4D3360063976E /* Project object */;

+ 36 - 0
test/dyn_limit_test.cxx

@@ -0,0 +1,36 @@
+//
+//  dyn_limit_test.cxx
+//  limit-test
+//
+//  Created by Sam Jaffe on 12/9/23.
+//
+
+#include "math/dyn_limit.h"
+
+#include "xcode_gtest_helper.h"
+
+TEST(DynBoundNumber, ConstructsInbounds) {
+  EXPECT_EQ(math::DynBound<int>(0, -5, +7), 0);
+}
+
+TEST(DynBoundNumber, ClampsOOBLow) {
+  EXPECT_EQ(math::DynBound<int>(-6, -5, +7), -5);
+}
+
+TEST(DynBoundNumber, ClampsOOBHigh) {
+  EXPECT_EQ(math::DynBound<int>(+8, -5, +7), +7);
+}
+
+TEST(DynBoundNumber, IncrementIsLimitSafe) {
+  math::DynBound<int> value(+7, -5, +7);
+  EXPECT_EQ(value, +7);
+  EXPECT_EQ(value++, +7);
+  EXPECT_EQ(value, +7);
+}
+
+TEST(DynBoundNumber, DecrementIsLimitSafe) {
+  math::DynBound<int> value(-5, -5, +7);
+  EXPECT_EQ(value, -5);
+  EXPECT_EQ(value--, -5);
+  EXPECT_EQ(value, -5);
+}

+ 31 - 47
test/limit_test.cxx

@@ -4,53 +4,37 @@
 //
 //  Created by Sam Jaffe on 2/3/17.
 //
-#pragma once
 
 #include "math/limit.h"
 
-#include <cxxtest/TestSuite.h>
-
-class bound_number_TestSuite : public CxxTest::TestSuite {
-public:
-  using number_t = bound_number<int, -5, +7>;
-  
-//  Fails to compile because of well-ordered principle
-//  void test_bounds_compiler_error() {
-//    bound_number<int, 0, -1>(0);
-//  }
-  
-  void test_number_construct_inbounds() {
-    number_t n{0};
-    TS_ASSERT_EQUALS(n, 0);
-  }
-  
-  void test_number_validate_oob_low() {
-    TS_ASSERT_THROWS((number_t{check_bounds, -6}), std::out_of_range);
-  }
-
-  void test_number_validate_oob_high() {
-    TS_ASSERT_THROWS((number_t{check_bounds, +8}), std::out_of_range);
-  }
-
-  void test_number_cannot_construct_oob_low() {
-    number_t n{-6};
-    TS_ASSERT_EQUALS(n, number_t::min);
-  }
-  
-  void test_number_cannot_construct_oob_high() {
-    number_t n{10};
-    TS_ASSERT_EQUALS(n, number_t::max);
-  }
-  
-  void test_number_cannot_increment_above_limit() {
-    number_t n{+7};
-    TS_ASSERT_EQUALS(n++, number_t::max);
-    TS_ASSERT_EQUALS(n, number_t::max);
-  }
-
-  void test_number_cannot_decrement_below_limit() {
-    number_t n{-5};
-    TS_ASSERT_EQUALS(n--, number_t::min);
-    TS_ASSERT_EQUALS(n, number_t::min);
-  }
-};
+#include "xcode_gtest_helper.h"
+
+using Number = math::Bound<int, -5, +7>;
+
+TEST(BoundNumber, ConstructsInbounds) { EXPECT_EQ(Number(0), 0); }
+
+TEST(BoundNumber, ValidateOOBLow) {
+  EXPECT_THROW(Number(math::assert_bounds, -6), std::out_of_range);
+}
+
+TEST(BoundNumber, ValidateOOBHigh) {
+  EXPECT_THROW(Number(math::assert_bounds, +8), std::out_of_range);
+}
+
+TEST(BoundNumber, ClampsOOBLow) { EXPECT_EQ(Number(-6), Number::min); }
+
+TEST(BoundNumber, ClampsOOBHigh) { EXPECT_EQ(Number(+8), Number::max); }
+
+TEST(BoundNumber, IncrementIsLimitSafe) {
+  Number value(Number::max);
+  EXPECT_EQ(value, Number::max);
+  EXPECT_EQ(value++, Number::max);
+  EXPECT_EQ(value, Number::max);
+}
+
+TEST(BoundNumber, DecrementIsLimitSafe) {
+  Number value(Number::min);
+  EXPECT_EQ(value, Number::min);
+  EXPECT_EQ(value--, Number::min);
+  EXPECT_EQ(value, Number::min);
+}

+ 42 - 0
test/xcode_gtest_helper.h

@@ -0,0 +1,42 @@
+//
+//  xcode_gtest_helper.h
+//
+//  Created by Sam Jaffe on 11/25/20.
+//  Copyright © 2020 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#if defined(__APPLE__)
+
+#if __has_include("printers.h")
+#include "printers.h"
+#endif
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header"
+#pragma clang diagnostic ignored "-Wcomma"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#pragma clang diagnostic pop
+
+#if defined(TARGET_OS_OSX)
+// This is a hack to allow XCode to properly display failures when running
+// unit tests.
+#undef EXPECT_THAT
+#define EXPECT_THAT ASSERT_THAT
+#undef EXPECT_THROW
+#define EXPECT_THROW ASSERT_THROW
+#undef EXPECT_ANY_THROW
+#define EXPECT_ANY_THROW ASSERT_ANY_THROW
+#undef EXPECT_NO_THROW
+#define EXPECT_NO_THROW ASSERT_NO_THROW
+#undef EXPECT_TRUE
+#define EXPECT_TRUE ASSERT_TRUE
+#undef EXPECT_FALSE
+#define EXPECT_FALSE ASSERT_FALSE
+
+#endif
+#endif