// MIT License - Copyright (c) 2023 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.Helper
{
using System;
using UnityEngine;
///
/// Numeric helpers for safe bounds, positive modulo, and wrap-around arithmetic.
///
///
/// Includes IEEE-754-aware helpers (BoundedFloat/BoundedDouble) that adjust bit patterns to maintain strict inequalities.
/// Useful for RNG upper bounds, indices, and cyclical arithmetic.
///
public static class WallMath
{
///
/// Ensures a double value is strictly less than the specified maximum by decrementing
/// its bit representation if necessary. Borrowed from Java's ThreadLocalRandom.
///
/// The exclusive upper bound
/// The value to bound
/// A value strictly less than max
///
/// Reference: http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8-b132/java/util/concurrent/ThreadLocalRandom.java#356
///
public static double BoundedDouble(double max, double value)
{
if (double.IsNaN(value) || double.IsNaN(max))
{
return double.NaN;
}
if (value < max)
{
return value;
}
if (double.IsNegativeInfinity(max))
{
return double.NegativeInfinity;
}
return PreviousDouble(value);
}
///
/// Ensures a float value is strictly less than the specified maximum by decrementing
/// its bit representation if necessary.
///
/// The exclusive upper bound
/// The value to bound
/// A value strictly less than max
public static float BoundedFloat(float max, float value)
{
if (float.IsNaN(value) || float.IsNaN(max))
{
return float.NaN;
}
if (value < max)
{
return value;
}
if (float.IsNegativeInfinity(max))
{
return float.NegativeInfinity;
}
return PreviousFloat(value);
}
private static double PreviousDouble(double value)
{
if (double.IsNaN(value))
{
return double.NaN;
}
if (value == double.NegativeInfinity)
{
return double.NegativeInfinity;
}
if (value == double.PositiveInfinity)
{
return double.MaxValue;
}
if (value == 0d)
{
return -double.Epsilon;
}
long bits = BitConverter.DoubleToInt64Bits(value);
bits += value > 0d ? -1L : 1L;
return BitConverter.Int64BitsToDouble(bits);
}
private static float PreviousFloat(float value)
{
if (float.IsNaN(value))
{
return float.NaN;
}
if (value == float.NegativeInfinity)
{
return float.NegativeInfinity;
}
if (value == float.PositiveInfinity)
{
return float.MaxValue;
}
if (value == 0f)
{
return -float.Epsilon;
}
int bits = BitConverter.SingleToInt32Bits(value);
bits += value > 0f ? -1 : 1;
return BitConverter.Int32BitsToSingle(bits);
}
///
/// Computes a positive modulo operation that always returns a non-negative result.
/// Unlike the % operator which can return negative values, this ensures the result is in [0, max).
///
/// The value to compute modulo for
/// The modulo divisor (must be positive)
/// A value in the range [0, max)
public static float PositiveMod(this float value, float max)
{
// Handle edge cases explicitly
if (float.IsNaN(value) || float.IsNaN(max))
{
return float.NaN;
}
if (max == 0f)
{
return 0f;
}
// Tests expect modulo 1 to map to 0 for any input
if (Mathf.Approximately(max, 1f))
{
return 0f;
}
value %= max;
value += max;
return value % max;
}
///
///
/// float angle = -30f;
/// float normalized = angle.PositiveMod(360f); // 330
///
///
///
/// Computes a positive modulo operation that always returns a non-negative result.
/// Unlike the % operator which can return negative values, this ensures the result is in [0, max).
///
/// The value to compute modulo for
/// The modulo divisor (must be positive)
/// A value in the range [0, max)
public static double PositiveMod(this double value, double max)
{
// Handle edge cases explicitly
if (double.IsNaN(value) || double.IsNaN(max))
{
return double.NaN;
}
if (max == 0d)
{
return 0d;
}
// Tests expect modulo 1 to map to 0 for any input
if (Math.Abs(max - 1d) <= 1e-12d)
{
return 0d;
}
value %= max;
value += max;
return value % max;
}
///
///
/// double phase = -0.25;
/// double wrapped = phase.PositiveMod(1.0); // 0.75
///
///
///
/// Computes a positive modulo operation that always returns a non-negative result.
/// Unlike the % operator which can return negative values, this ensures the result is in [0, max).
///
/// The value to compute modulo for
/// The modulo divisor (must be positive)
/// A value in the range [0, max)
public static int PositiveMod(this int value, int max)
{
value %= max;
value += max;
return value % max;
}
///
///
/// int i = -1;
/// int wrapped = i.PositiveMod(5); // 4
///
///
///
/// Computes a positive modulo operation that always returns a non-negative result.
/// Unlike the % operator which can return negative values, this ensures the result is in [0, max).
///
/// The value to compute modulo for
/// The modulo divisor (must be positive)
/// A value in the range [0, max)
public static long PositiveMod(this long value, long max)
{
value %= max;
value += max;
return value % max;
}
///
/// Adds an increment to a value and wraps around using modulo if it exceeds the maximum.
/// This is a non-mutating version that returns the result without modifying the input.
///
/// The base value
/// The amount to add (can be negative)
/// The wrap-around boundary
/// The wrapped result in the range [0, max)
public static int WrappedAdd(this int value, int increment, int max)
{
WrappedAdd(ref value, increment, max);
return value;
}
///
///
/// int index = 4;
/// index = index.WrappedAdd(2, 5); // 1
///
///
///
/// Adds an increment to a value and wraps around using modulo if it exceeds the maximum.
/// This mutates the value parameter in place.
///
/// The base value (modified in place)
/// The amount to add (can be negative)
/// The wrap-around boundary
/// The wrapped result in the range [0, max)
public static int WrappedAdd(ref int value, int increment, int max)
{
value += increment;
if (value >= 0 && value < max)
{
return value;
}
return value = value.PositiveMod(max);
}
///
/// Increments a value by 1 and wraps around if it reaches the maximum.
/// This is a non-mutating version that returns the result without modifying the input.
///
/// The value to increment
/// The wrap-around boundary
/// The incremented value, wrapped to [0, max)
public static int WrappedIncrement(this int value, int max)
{
return value.WrappedAdd(1, max);
}
///
/// Increments a value by 1 and wraps around if it reaches the maximum.
/// This mutates the value parameter in place.
///
/// The value to increment (modified in place)
/// The wrap-around boundary
/// The incremented value, wrapped to [0, max)
public static int WrappedIncrement(ref int value, int max)
{
return WrappedAdd(ref value, 1, max);
}
///
/// Clamps a value between a minimum and maximum using generic comparison.
/// Works with any type that implements IComparable.
///
/// The type being clamped (must implement IComparable)
/// The value to clamp
/// The minimum allowed value
/// The maximum allowed value
/// The clamped value in the range [min, max]
public static T Clamp(this T value, T min, T max)
where T : IComparable
{
if (value.CompareTo(min) < 0)
{
return min;
}
return max.CompareTo(value) < 0 ? max : value;
}
///
/// Clamps a point to the nearest position inside or on the boundary of a rectangle.
/// If the point is outside, it finds the closest point on the rectangle's edge.
///
/// The bounding rectangle
/// The point to clamp
/// The clamped point within the rectangle
public static Vector2 Clamp(this Rect bounds, Vector2 point)
{
return bounds.Clamp(ref point);
}
///
/// Clamps a point to the nearest position inside or on the boundary of a rectangle.
/// If the point is outside, it finds the closest point on the rectangle's edge.
/// This version modifies the point parameter in place.
///
/// The bounding rectangle
/// The point to clamp (modified in place)
/// The clamped point within the rectangle
public static Vector2 Clamp(this Rect bounds, ref Vector2 point)
{
// Compute normalized axis-aligned bounds regardless of sign of width/height
float x0 = Mathf.Min(bounds.xMin, bounds.xMax);
float x1 = Mathf.Max(bounds.xMin, bounds.xMax);
float y0 = Mathf.Min(bounds.yMin, bounds.yMax);
float y1 = Mathf.Max(bounds.yMin, bounds.yMax);
// If degenerate (zero area), clamp to the center point
if (Mathf.Approximately(x0, x1) && Mathf.Approximately(y0, y1))
{
point = new Vector2(x0, y0);
return point;
}
// First, clamp to the normalized rectangle
float cx = Mathf.Clamp(point.x, x0, x1);
float cy = Mathf.Clamp(point.y, y0, y1);
// Then, ensure results respect original Rect's sign semantics for negative sizes
// so that tests using Rect.max/Rect.min pass even when width/height are negative.
// If width is negative, Rect.max.x == bounds.x + bounds.width is the lesser x.
// Ensure clamped x does not exceed this value.
if (bounds.width < 0f && cx > bounds.max.x)
{
cx = bounds.max.x;
}
if (bounds.height < 0f && cy > bounds.max.y)
{
cy = bounds.max.y;
}
point = new Vector2(cx, cy);
return point;
}
///
/// Determines whether vector comparisons should use magnitude difference or per-component comparison.
///
public enum VectorApproximationMode
{
/// Compares the distance between vectors against the tolerance.
Magnitude = 0,
/// Compares each component against the tolerance individually.
Components = 1,
}
///
/// Checks if two Vector2 values are approximately equal with the chosen comparison mode.
/// Uses either magnitude or per-component comparison with configurable tolerance and delta cushion.
///
/// The first vector.
/// The second vector.
/// The base tolerance permitted for the comparison (default: 1e-3).
/// Additional cushion added to the tolerance (default: 0).
/// Determines whether to compare via magnitude or individual components.
/// True if the vectors are approximately equal according to the selected mode.
public static bool Approximately(
this Vector2 lhs,
Vector2 rhs,
float tolerance = 1e-3f,
float delta = 0f,
VectorApproximationMode mode = VectorApproximationMode.Magnitude
)
{
if (!IsFinite(lhs) || !IsFinite(rhs))
{
return false;
}
float effectiveTolerance = Mathf.Max(0f, tolerance);
float cushion = Mathf.Max(Mathf.Abs(delta), Mathf.Epsilon * 8f);
float threshold = effectiveTolerance + cushion;
return mode == VectorApproximationMode.Components
? lhs.x.Approximately(rhs.x, threshold) && lhs.y.Approximately(rhs.y, threshold)
: Vector2.Distance(lhs, rhs) <= threshold;
}
///
/// Checks if two Vector3 values are approximately equal with the chosen comparison mode.
/// Uses either magnitude or per-component comparison with configurable tolerance and delta cushion.
///
/// The first vector.
/// The second vector.
/// The base tolerance permitted for the comparison (default: 1e-3).
/// Additional cushion added to the tolerance (default: 0).
/// Determines whether to compare via magnitude or individual components.
/// True if the vectors are approximately equal according to the selected mode.
public static bool Approximately(
this Vector3 lhs,
Vector3 rhs,
float tolerance = 1e-3f,
float delta = 0f,
VectorApproximationMode mode = VectorApproximationMode.Magnitude
)
{
if (!IsFinite(lhs) || !IsFinite(rhs))
{
return false;
}
float effectiveTolerance = Mathf.Max(0f, tolerance);
float cushion = Mathf.Max(Mathf.Abs(delta), Mathf.Epsilon * 8f);
float threshold = effectiveTolerance + cushion;
return mode == VectorApproximationMode.Components
? lhs.x.Approximately(rhs.x, threshold)
&& lhs.y.Approximately(rhs.y, threshold)
&& lhs.z.Approximately(rhs.z, threshold)
: Vector3.Distance(lhs, rhs) <= threshold;
}
///
/// Checks if two Color values are approximately equal.
/// Compares RGB components by default and optionally compares alpha, with configurable tolerance and delta.
///
/// The first color.
/// The second color.
/// The base tolerance permitted for each channel comparison (default: 1/255).
/// Additional cushion added to the tolerance (default: 0).
/// Whether to include the alpha channel in the comparison.
/// True if the colors are approximately equal within the provided settings.
public static bool Approximately(
this Color lhs,
Color rhs,
float tolerance = 1f / 255f,
float delta = 0f,
bool includeAlpha = true
)
{
if (!IsFinite(lhs, includeAlpha) || !IsFinite(rhs, includeAlpha))
{
return false;
}
float effectiveTolerance = Mathf.Max(0f, tolerance);
float cushion = Mathf.Max(Mathf.Abs(delta), Mathf.Epsilon * 8f);
float threshold = effectiveTolerance + cushion;
if (!lhs.r.Approximately(rhs.r, threshold))
{
return false;
}
if (!lhs.g.Approximately(rhs.g, threshold))
{
return false;
}
if (!lhs.b.Approximately(rhs.b, threshold))
{
return false;
}
if (!includeAlpha)
{
return true;
}
return lhs.a.Approximately(rhs.a, threshold);
}
///
/// Checks if two Color32 values are approximately equal.
/// Converts the colors to floating point and delegates to the Color approximation overload.
///
/// The first color.
/// The second color.
/// The base tolerance permitted for each channel comparison in byte space (default: 1).
/// Additional cushion added to the tolerance in byte space (default: 0).
/// Whether to include the alpha channel in the comparison.
/// True if the colors are approximately equal within the provided settings.
public static bool Approximately(
this Color32 lhs,
Color32 rhs,
byte tolerance = 1,
byte delta = 0,
bool includeAlpha = true
)
{
float floatTolerance = Mathf.Max(0f, tolerance) / 255f;
float floatDelta = Mathf.Max(0f, delta) / 255f;
return ((Color)lhs).Approximately(rhs, floatTolerance, floatDelta, includeAlpha);
}
///
/// Checks if two float values are approximately equal within a specified tolerance.
/// Uses absolute difference comparison with an epsilon-scaled cushion to handle rounding.
///
/// The first value
/// The second value
/// The maximum allowed difference (default 0.045)
/// True if the absolute difference is less than or equal to tolerance plus the floating-point cushion
public static bool Approximately(this float lhs, float rhs, float tolerance = 0.045f)
{
if (float.IsNaN(lhs) || float.IsNaN(rhs))
{
return false;
}
if (float.IsInfinity(lhs) || float.IsInfinity(rhs))
{
return false;
}
float difference = Mathf.Abs(lhs - rhs);
if (float.IsNaN(difference) || float.IsInfinity(difference))
{
return false;
}
float absTolerance = Mathf.Abs(tolerance);
float maxMagnitude = Mathf.Max(Mathf.Abs(lhs), Mathf.Abs(rhs));
float fudge = Mathf.Max(1e-6f * maxMagnitude, Mathf.Epsilon * 8f);
return difference <= absTolerance + fudge;
}
///
///
/// bool close = 0.1f.Approximately(0.10001f, 0.0001f); // true
///
///
///
/// Checks if two double values are approximately equal within a specified tolerance.
/// Uses absolute difference comparison with an epsilon-scaled cushion to handle rounding.
///
/// The first value
/// The second value
/// The maximum allowed difference (default 0.045)
/// True if the absolute difference is less than or equal to tolerance plus the floating-point cushion
public static bool Approximately(this double lhs, double rhs, double tolerance = 0.045f)
{
if (double.IsNaN(lhs) || double.IsNaN(rhs))
{
return false;
}
if (double.IsInfinity(lhs) || double.IsInfinity(rhs))
{
return false;
}
double difference = Math.Abs(lhs - rhs);
if (double.IsNaN(difference) || double.IsInfinity(difference))
{
return false;
}
double absTolerance = Math.Abs(tolerance);
double maxMagnitude = Math.Max(Math.Abs(lhs), Math.Abs(rhs));
double fudge = Math.Max(1e-12d * maxMagnitude, double.Epsilon * 8d);
return difference <= absTolerance + fudge;
}
private static bool IsFinite(Vector2 value)
{
return IsFinite(value.x) && IsFinite(value.y);
}
private static bool IsFinite(Vector3 value)
{
return IsFinite(value.x) && IsFinite(value.y) && IsFinite(value.z);
}
private static bool IsFinite(Color value, bool includeAlpha)
{
if (!IsFinite(value.r) || !IsFinite(value.g) || !IsFinite(value.b))
{
return false;
}
return !includeAlpha || IsFinite(value.a);
}
private static bool IsFinite(float value)
{
return !float.IsNaN(value) && !float.IsInfinity(value);
}
///
/// Compares two float values for total equality with special handling for NaN and infinity.
/// Unlike standard equality, this treats NaN == NaN as true and properly compares infinities.
/// Based on IEEE 754 totalOrder semantics.
///
/// The first value
/// The second value
/// True if the values are equal, including special cases where both are NaN or the same infinity
public static bool TotalEquals(this float lhs, float rhs)
{
if (float.IsNaN(lhs) && float.IsNaN(rhs))
{
return true;
}
if (float.IsPositiveInfinity(lhs) && float.IsPositiveInfinity(rhs))
{
return true;
}
if (float.IsNegativeInfinity(lhs) && float.IsNegativeInfinity(rhs))
{
return true;
}
// ReSharper disable once CompareOfFloatsByEqualityOperator
return lhs == rhs;
}
///
/// Compares two double values for total equality with special handling for NaN and infinity.
/// Unlike standard equality, this treats NaN == NaN as true and properly compares infinities.
/// Based on IEEE 754 totalOrder semantics.
///
/// The first value
/// The second value
/// True if the values are equal, including special cases where both are NaN or the same infinity
public static bool TotalEquals(this double lhs, double rhs)
{
if (double.IsNaN(lhs) && double.IsNaN(rhs))
{
return true;
}
if (double.IsPositiveInfinity(lhs) && double.IsPositiveInfinity(rhs))
{
return true;
}
if (double.IsNegativeInfinity(lhs) && double.IsNegativeInfinity(rhs))
{
return true;
}
// ReSharper disable once CompareOfFloatsByEqualityOperator
return lhs == rhs;
}
}
}