diff --git a/examples/js_dsl/mod.test.ts b/examples/js_dsl/mod.test.ts index d3bf1bf..b348ede 100644 --- a/examples/js_dsl/mod.test.ts +++ b/examples/js_dsl/mod.test.ts @@ -59,6 +59,18 @@ describe("primitive types", () => { expectTypeErrorWithMessage(() => mod.doubleNumber("21"), "Argument 1 must be a number"); }); + it("exactU64 accepts safe unsigned integers", () => { + expect(mod.exactU64(0)).toEqual(0); + expect(mod.exactU64(2 ** 32)).toEqual(2 ** 32); + expect(mod.exactU64(Number.MAX_SAFE_INTEGER)).toEqual(Number.MAX_SAFE_INTEGER); + }); + + it("exactU64 rejects values that are not exact u64 integers", () => { + for (const value of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1, Infinity, -Infinity, NaN]) { + expect(() => mod.exactU64(value), `value ${value}`).toThrow("InvalidUnsignedInteger"); + } + }); + it("toggleBool", () => { expect(mod.toggleBool(true)).toBe(false); expect(mod.toggleBool(false)).toBe(true); diff --git a/examples/js_dsl/mod.zig b/examples/js_dsl/mod.zig index d3f655b..23e928c 100644 --- a/examples/js_dsl/mod.zig +++ b/examples/js_dsl/mod.zig @@ -79,6 +79,11 @@ pub fn largeUnsignedBoundary() Number { return Number.from(@as(u64, std.math.maxInt(i64)) + 1); } +/// Return a number validated as an exact u64 by `toU64Exact`. +pub fn exactU64(n: Number) !Number { + return Number.from(try n.toU64Exact()); +} + /// Negate a boolean. pub fn toggleBool(b: Boolean) Boolean { return Boolean.from(!b.assertBool()); diff --git a/src/js/number.zig b/src/js/number.zig index 79275ec..eeb08dc 100644 --- a/src/js/number.zig +++ b/src/js/number.zig @@ -61,6 +61,27 @@ pub const Number = struct { return @intFromFloat(value); } + /// Attempts to convert the JavaScript number to a `u64` without coercion. + /// + /// Returns `error.InvalidUnsignedInteger` if the number is negative, + /// fractional, non-finite, or greater than `Number.MAX_SAFE_INTEGER` + /// (2^53 - 1). + /// + /// Larger values cannot be represented exactly by a JS number; + /// use `BigInt.toU64` for the full `u64` range. + pub fn toU64Exact(self: Number) !u64 { + const max: f64 = @floatFromInt(std.math.maxInt(u53)); + const value = try self.toF64(); + if (!std.math.isFinite(value) or + value < 0 or + value > max or + @trunc(value) != value) + { + return error.InvalidUnsignedInteger; + } + return @intFromFloat(value); + } + /// Attempts to convert the JavaScript number to a Zig `f64`. /// /// This conversion is generally lossless for most JS numbers, which are typically `f64`.