diff --git a/PowerKit.Tests/Extensions/DoubleExtensionsTests.cs b/PowerKit.Tests/Extensions/DoubleExtensionsTests.cs index 06a7e7e..ad80928 100644 --- a/PowerKit.Tests/Extensions/DoubleExtensionsTests.cs +++ b/PowerKit.Tests/Extensions/DoubleExtensionsTests.cs @@ -27,4 +27,17 @@ public void ParseOrDefault_Test() double.ParseOrDefault("abc", -1.0).Should().Be(-1.0); double.ParseOrDefault(null).Should().Be(0.0); } + + [Fact] + public void Wrap_Test() + { + // Act & assert + 5.0.Wrap(0.0, 10.0).Should().Be(5.0); + 13.0.Wrap(0.0, 10.0).Should().Be(3.0); + (-3.0).Wrap(0.0, 10.0).Should().Be(7.0); + (-10.0).Wrap(0.0, 10.0).Should().Be(0.0); + 0.0.Wrap(0.0, 10.0).Should().Be(0.0); + 10.0.Wrap(0.0, 10.0).Should().Be(0.0); + 23.0.Wrap(0.0, 10.0).Should().Be(3.0); + } } diff --git a/PowerKit.Tests/Extensions/SingleExtensionsTests.cs b/PowerKit.Tests/Extensions/SingleExtensionsTests.cs index 0c847f2..36df17a 100644 --- a/PowerKit.Tests/Extensions/SingleExtensionsTests.cs +++ b/PowerKit.Tests/Extensions/SingleExtensionsTests.cs @@ -31,4 +31,17 @@ public void ParseOrDefault_Test() float.ParseOrDefault("abc", -1.0f).Should().Be(-1.0f); float.ParseOrDefault(null).Should().Be(0.0f); } + + [Fact] + public void Wrap_Test() + { + // Act & assert + 5.0f.Wrap(0.0f, 10.0f).Should().Be(5.0f); + 13.0f.Wrap(0.0f, 10.0f).Should().Be(3.0f); + (-3.0f).Wrap(0.0f, 10.0f).Should().Be(7.0f); + (-10.0f).Wrap(0.0f, 10.0f).Should().Be(0.0f); + 0.0f.Wrap(0.0f, 10.0f).Should().Be(0.0f); + 10.0f.Wrap(0.0f, 10.0f).Should().Be(0.0f); + 23.0f.Wrap(0.0f, 10.0f).Should().Be(3.0f); + } } diff --git a/PowerKit/Extensions/DoubleExtensions.cs b/PowerKit/Extensions/DoubleExtensions.cs index 40e8e1f..2a434fd 100644 --- a/PowerKit/Extensions/DoubleExtensions.cs +++ b/PowerKit/Extensions/DoubleExtensions.cs @@ -66,4 +66,31 @@ public static double ParseOrDefault( public static double ParseOrDefault(string? str, double defaultValue = default) => double.ParseOrDefault(str, CultureInfo.CurrentCulture, defaultValue); } + + extension(double value) + { + /// + /// Wraps the value into the half-open range [, ). + /// A value equal to wraps to . + /// + /// The inclusive lower bound of the range. + /// The exclusive upper bound of the range. + /// + /// This method requires to be greater than . + /// If is less than or equal to , the behavior is invalid. + /// + public double Wrap(double min, double max) + { + if (max <= min) + { + throw new ArgumentOutOfRangeException( + nameof(max), + "The maximum value must be greater than the minimum value." + ); + } + + var range = max - min; + return min + (((value - min) % range + range) % range); + } + } } diff --git a/PowerKit/Extensions/SingleExtensions.cs b/PowerKit/Extensions/SingleExtensions.cs index a6bbd3f..2862b63 100644 --- a/PowerKit/Extensions/SingleExtensions.cs +++ b/PowerKit/Extensions/SingleExtensions.cs @@ -66,4 +66,31 @@ public static float ParseOrDefault( public static float ParseOrDefault(string? str, float defaultValue = default) => float.ParseOrDefault(str, CultureInfo.CurrentCulture, defaultValue); } + + extension(float value) + { + /// + /// Wraps the value into the half-open range [, ), + /// cycling it back around when it exceeds the bounds. + /// + /// + /// The returned value is greater than or equal to and less than + /// for valid ranges. A value equal to + /// wraps to . + /// When is less than or equal to , this + /// method does not throw; it follows floating-point arithmetic and may return + /// . + /// + public float Wrap(float min, float max) + { + if (max <= min) + { + throw new ArgumentOutOfRangeException(nameof(max), "max must be greater than min."); + } + + var range = max - min; + var normalized = ((value - min) % range + range) % range; + return min + normalized; + } + } }