Commit 1a7a711964

expikr <77922942+expikr@users.noreply.github.com>
2024-01-15 02:45:39
std.math: add `wrap` function (#18154)
1 parent 8afafa7
Changed files (1)
lib
lib/std/math.zig
@@ -428,6 +428,100 @@ pub const min3 = @compileError("deprecated; use @min instead");
 pub const max3 = @compileError("deprecated; use @max instead");
 pub const ln = @compileError("deprecated; use @log instead");
 
+/// Odd sawtooth function
+/// ```
+///         |
+///      /  | /    /
+///     /   |/    /
+///  --/----/----/--
+///   /    /|   /
+///  /    / |  /
+///         |
+/// ```
+/// Limit x to the half-open interval [-r, r).
+pub fn wrap(x: anytype, r: anytype) @TypeOf(x) {
+    const info_x = @typeInfo(@TypeOf(x));
+    const info_r = @typeInfo(@TypeOf(r));
+    if (info_x == .Int and info_x.Int.signedness != .signed) {
+        @compileError("x must be floating point, comptime integer, or signed integer.");
+    }
+    switch (info_r) {
+        .Int => {
+            // in the rare usecase of r not being comptime_int or float,
+            // take the penalty of having an intermediary type conversion,
+            // otherwise the alternative is to unwind iteratively to avoid overflow
+            const R = comptime do: {
+                var info = info_r;
+                info.Int.bits += 1;
+                info.Int.signedness = .signed;
+                break :do @Type(info);
+            };
+            const radius: if (info_r.Int.signedness == .signed) @TypeOf(r) else R = r;
+            return @intCast(@mod(x - radius, 2 * @as(R, r)) - r); // provably impossible to overflow
+        },
+        else => {
+            return @mod(x - r, 2 * r) - r;
+        },
+    }
+}
+test "wrap" {
+    // Within range
+    try testing.expect(wrap(@as(i32, -75), @as(i32, 180)) == -75);
+    try testing.expect(wrap(@as(i32, -75), @as(i32, -180)) == -75);
+    // Below
+    try testing.expect(wrap(@as(i32, -225), @as(i32, 180)) == 135);
+    try testing.expect(wrap(@as(i32, -225), @as(i32, -180)) == 135);
+    // Above
+    try testing.expect(wrap(@as(i32, 361), @as(i32, 180)) == 1);
+    try testing.expect(wrap(@as(i32, 361), @as(i32, -180)) == 1);
+
+    // One period, right limit, positive r
+    try testing.expect(wrap(@as(i32, 180), @as(i32, 180)) == -180);
+    // One period, left limit, positive r
+    try testing.expect(wrap(@as(i32, -180), @as(i32, 180)) == -180);
+    // One period, right limit, negative r
+    try testing.expect(wrap(@as(i32, 180), @as(i32, -180)) == 180);
+    // One period, left limit, negative r
+    try testing.expect(wrap(@as(i32, -180), @as(i32, -180)) == 180);
+
+    // Two periods, right limit, positive r
+    try testing.expect(wrap(@as(i32, 540), @as(i32, 180)) == -180);
+    // Two periods, left limit, positive r
+    try testing.expect(wrap(@as(i32, -540), @as(i32, 180)) == -180);
+    // Two periods, right limit, negative r
+    try testing.expect(wrap(@as(i32, 540), @as(i32, -180)) == 180);
+    // Two periods, left limit, negative r
+    try testing.expect(wrap(@as(i32, -540), @as(i32, -180)) == 180);
+
+    // Floating point
+    try testing.expect(wrap(@as(f32, 1.125), @as(f32, 1.0)) == -0.875);
+    try testing.expect(wrap(@as(f32, -127.5), @as(f32, 180)) == -127.5);
+
+    // Mix of comptime and non-comptime
+    var i: i32 = 1;
+    _ = &i;
+    try testing.expect(wrap(i, 10) == 1);
+}
+test wrap {
+    const limit: i32 = 180;
+    // Within range
+    try testing.expect(wrap(@as(i32, -75), limit) == -75);
+    // Below
+    try testing.expect(wrap(@as(i32, -225), limit) == 135);
+    // Above
+    try testing.expect(wrap(@as(i32, 361), limit) == 1);
+}
+
+/// Odd ramp function
+/// ```
+///         |  _____
+///         | /
+///         |/
+///  -------/-------
+///        /|
+///  _____/ |
+///         |
+/// ```
 /// Limit val to the inclusive range [lower, upper].
 pub fn clamp(val: anytype, lower: anytype, upper: anytype) @TypeOf(val, lower, upper) {
     assert(lower <= upper);