Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ json:
jsonc:
- changed-files:
- any-glob-to-any-file: jsonc/**
math:
- changed-files:
- any-glob-to-any-file: math/**
media-types:
- changed-files:
- any-glob-to-any-file: media_types/**
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ jobs:
io(/unstable)?
json(/unstable)?
jsonc(/unstable)?
math(/unstable)?
media-types(/unstable)?
msgpack(/unstable)?
net(/unstable)?
Expand Down
1 change: 1 addition & 0 deletions browser-compat.tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"./io",
"./json",
"./jsonc",
"./math",
"./media_types",
"./msgpack",
"./net",
Expand Down
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"./io",
"./json",
"./jsonc",
"./math",
"./media_types",
"./msgpack",
"./net",
Expand Down
1 change: 1 addition & 0 deletions import_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@std/io": "jsr:@std/io@^0.225.2",
"@std/json": "jsr:@std/json@^1.0.2",
"@std/jsonc": "jsr:@std/jsonc@^1.0.2",
"@std/math": "jsr:@std/math@^0.0.0",
"@std/media-types": "jsr:@std/media-types@^1.1.0",
"@std/msgpack": "jsr:@std/msgpack@^1.0.3",
"@std/net": "jsr:@std/net@^1.0.6",
Expand Down
27 changes: 27 additions & 0 deletions math/clamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// This module is browser compatible.

/**
* Clamp a number within the inclusive [min, max] range.
*
* @param num The number to be clamped
* @param limits The inclusive [min, max] range
* @returns The clamped number
*
* @example Usage
* ```ts
* import { clamp } from "@std/math/clamp";
* import { assertEquals } from "@std/assert";
* assertEquals(clamp(5, [1, 10]), 5);
* assertEquals(clamp(-5, [1, 10]), 1);
* assertEquals(clamp(15, [1, 10]), 10);
* ```
*/
export function clamp(num: number, limits: [min: number, max: number]): number {
const [min, max] = limits;
if (min > max) {
Comment on lines +20 to +22
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Passing limits as an array means that this array needs to be allocated prior to passing it to this function. Given that math functions are often used in high performance scenarios it might be more desirable to avoid the need to allocate at all for determining whether a value is in-between two others.

throw new RangeError("`min` must be less than or equal to `max`");
}

return Math.min(Math.max(num, min), max);
}
46 changes: 46 additions & 0 deletions math/clamp_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { clamp } from "./clamp.ts";
import { assert, assertEquals, assertThrows } from "@std/assert";

Deno.test("clamp()", async (t) => {
await t.step("basic functionality", () => {
assertEquals(clamp(5, [1, 10]), 5);
assertEquals(clamp(-5, [1, 10]), 1);
assertEquals(clamp(15, [1, 10]), 10);
});

await t.step("NaN", () => {
assertEquals(clamp(NaN, [0, 1]), NaN);
assertEquals(clamp(0, [NaN, 0]), NaN);
assertEquals(clamp(0, [0, NaN]), NaN);
});

await t.step("infinities", () => {
assertEquals(clamp(5, [0, Infinity]), 5);
assertEquals(clamp(-5, [-Infinity, 10]), -5);
});

await t.step("+/-0", () => {
assert(Object.is(clamp(0, [0, 1]), 0));
assert(Object.is(clamp(-0, [-0, 1]), -0));

assert(Object.is(clamp(-2, [0, 1]), 0));
assert(Object.is(clamp(-2, [-0, 1]), -0));
assert(Object.is(clamp(2, [-1, 0]), 0));
assert(Object.is(clamp(2, [-1, -0]), -0));

assert(Object.is(clamp(-0, [0, 1]), 0));
assert(Object.is(clamp(0, [-0, 1]), 0));

assert(Object.is(clamp(-0, [-1, 0]), -0));
assert(Object.is(clamp(0, [-1, -0]), -0));
});

await t.step("errors", () => {
assertThrows(
() => clamp(5, [10, 1]),
RangeError,
"`min` must be less than or equal to `max`",
);
});
});
10 changes: 10 additions & 0 deletions math/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@std/math",
"version": "0.0.0",
"exports": {
".": "./mod.ts",
"./clamp": "./clamp.ts",
"./modulo": "./modulo.ts",
"./round-to": "./round_to.ts"
}
}
24 changes: 24 additions & 0 deletions math/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// This module is browser compatible.

/**
* Math functions such as modulo and clamp.
*
* ```ts
* import { clamp, modulo } from "@std/math";
* import { assertEquals } from "@std/assert";
*
* for (let n = -3; n <= 3; ++n) {
* const val = n * 12 + 5;
* // 5 o'clock is always 5 o'clock, no matter how many twelve-hour cycles you add or remove
* assertEquals(modulo(val, 12), 5);
* assertEquals(clamp(val, [0, 11]), n === 0 ? 5 : n > 0 ? 11 : 0);
* }
* ```
*
* @module
*/

export * from "./clamp.ts";
export * from "./modulo.ts";
export * from "./round_to.ts";
32 changes: 32 additions & 0 deletions math/modulo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// This module is browser compatible.

/**
* Computes the floored modulo of a number.
*
* @param num The number to be reduced
* @param modulus The modulus
* @returns The reduced number
*
* @example Usage
* ```ts
* import { modulo } from "@std/math/modulo";
* import { assertEquals } from "@std/assert";
*
* for (let n = -3; n <= 3; ++n) {
* const val = n * 12 + 5;
* // 5 o'clock is always 5 o'clock, no matter how many twelve-hour cycles you add or remove
* assertEquals(modulo(val, 12), 5);
* }
* ```
*/
export function modulo(num: number, modulus: number): number {
if (!Number.isFinite(num) || Number.isNaN(modulus) || modulus === 0) {
return NaN;
}
if (num === 0) return modulus < 0 ? -0 : 0;
if (modulus === Infinity) return num < 0 ? Infinity : num;
if (modulus === -Infinity) return num > 0 ? -Infinity : num;

return (num % modulus + modulus) % modulus;
}
Comment on lines +23 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's an alternative implementation for modulo.

export function modulo(num: number, modulus: number): number {
  num %= modulus;
  if (num === 0) {
    num = modulus < 0 ? -0 : 0;
  } else if ((num < 0) !== (modulus < 0)) {
    num += modulus;
  }
  return num;
}

In my benchmarks (code below), it was faster than the current implementation in the common case where both inputs are finite and nonzero, although slower when either input is infinite, NaN or 0. All tests still pass.

// `baseline` and `modulo` are different implementations.
// `cases` is taken from the python parity test.
const sink = new Set<number>();
for (const [a, b] of cases) {
  const group = Deno.inspect([a, b]);

  Deno.bench("baseline", { group }, () => {
    sink.add(baseline(a, b));
  });

  Deno.bench("modulo", { group }, () => {
    sink.add(modulo(a, b));
  });
}

179 changes: 179 additions & 0 deletions math/modulo_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { modulo } from "./modulo.ts";
import { assert, assertEquals } from "@std/assert";

Deno.test("modulo()", async (t) => {
await t.step("basic functionality", async (t) => {
for (let n = -3; n <= 3; ++n) {
const val = n * 12 + 5;
await t.step(`modulo(${val}, 12) == 5`, () => {
assertEquals(modulo(val, 12), 5);
});
}
});

await t.step("non-integer values", () => {
assertEquals(modulo(5.5, 2), 1.5);
assertEquals(modulo(-5.5, 2), 0.5);
assertEquals(modulo(5, 0.5), 0);
assertEquals(modulo(-5.5, 0.5), 0);
});

await t.step("edge cases", () => {
assertEquals(modulo(NaN, 5), NaN);
assertEquals(modulo(5, NaN), NaN);
assertEquals(modulo(Infinity, 5), NaN);
assertEquals(modulo(-Infinity, 5), NaN);
assertEquals(modulo(5, Infinity), 5);
assertEquals(modulo(5, -Infinity), -Infinity);
assertEquals(modulo(5, 0), NaN);
assertEquals(modulo(5, -0), NaN);
assert(Object.is(modulo(0, 5), 0));
assert(Object.is(modulo(-0, 5), 0));
});

await t.step("parity with python `%` operator (floored modulo)", () => {
/**
* ```python
* def modulo(a, b):
* try: return a % b
* except: return float("nan")
* xs = [float('inf'), float('-inf'), float('nan'), 0.0, -0.0, 0.5, 1.0, 2.0, -0.5, -1.0, -2.0]
* cases = [(a, b, modulo(a, b)) for a in xs for b in xs]
* ```
*/
const cases: [a: number, b: number, result: number][] = [
[Infinity, Infinity, NaN],
[Infinity, -Infinity, NaN],
[Infinity, NaN, NaN],
[Infinity, 0.0, NaN],
[Infinity, -0.0, NaN],
[Infinity, 0.5, NaN],
[Infinity, 1.0, NaN],
[Infinity, 2.0, NaN],
[Infinity, -0.5, NaN],
[Infinity, -1.0, NaN],
[Infinity, -2.0, NaN],
[-Infinity, Infinity, NaN],
[-Infinity, -Infinity, NaN],
[-Infinity, NaN, NaN],
[-Infinity, 0.0, NaN],
[-Infinity, -0.0, NaN],
[-Infinity, 0.5, NaN],
[-Infinity, 1.0, NaN],
[-Infinity, 2.0, NaN],
[-Infinity, -0.5, NaN],
[-Infinity, -1.0, NaN],
[-Infinity, -2.0, NaN],
[NaN, Infinity, NaN],
[NaN, -Infinity, NaN],
[NaN, NaN, NaN],
[NaN, 0.0, NaN],
[NaN, -0.0, NaN],
[NaN, 0.5, NaN],
[NaN, 1.0, NaN],
[NaN, 2.0, NaN],
[NaN, -0.5, NaN],
[NaN, -1.0, NaN],
[NaN, -2.0, NaN],
[0.0, Infinity, 0.0],
[0.0, -Infinity, -0.0],
[0.0, NaN, NaN],
[0.0, 0.0, NaN],
[0.0, -0.0, NaN],
[0.0, 0.5, 0.0],
[0.0, 1.0, 0.0],
[0.0, 2.0, 0.0],
[0.0, -0.5, -0.0],
[0.0, -1.0, -0.0],
[0.0, -2.0, -0.0],
[-0.0, Infinity, 0.0],
[-0.0, -Infinity, -0.0],
[-0.0, NaN, NaN],
[-0.0, 0.0, NaN],
[-0.0, -0.0, NaN],
[-0.0, 0.5, 0.0],
[-0.0, 1.0, 0.0],
[-0.0, 2.0, 0.0],
[-0.0, -0.5, -0.0],
[-0.0, -1.0, -0.0],
[-0.0, -2.0, -0.0],
[0.5, Infinity, 0.5],
[0.5, -Infinity, -Infinity],
[0.5, NaN, NaN],
[0.5, 0.0, NaN],
[0.5, -0.0, NaN],
[0.5, 0.5, 0.0],
[0.5, 1.0, 0.5],
[0.5, 2.0, 0.5],
[0.5, -0.5, -0.0],
[0.5, -1.0, -0.5],
[0.5, -2.0, -1.5],
[1.0, Infinity, 1.0],
[1.0, -Infinity, -Infinity],
[1.0, NaN, NaN],
[1.0, 0.0, NaN],
[1.0, -0.0, NaN],
[1.0, 0.5, 0.0],
[1.0, 1.0, 0.0],
[1.0, 2.0, 1.0],
[1.0, -0.5, -0.0],
[1.0, -1.0, -0.0],
[1.0, -2.0, -1.0],
[2.0, Infinity, 2.0],
[2.0, -Infinity, -Infinity],
[2.0, NaN, NaN],
[2.0, 0.0, NaN],
[2.0, -0.0, NaN],
[2.0, 0.5, 0.0],
[2.0, 1.0, 0.0],
[2.0, 2.0, 0.0],
[2.0, -0.5, -0.0],
[2.0, -1.0, -0.0],
[2.0, -2.0, -0.0],
[-0.5, Infinity, Infinity],
[-0.5, -Infinity, -0.5],
[-0.5, NaN, NaN],
[-0.5, 0.0, NaN],
[-0.5, -0.0, NaN],
[-0.5, 0.5, 0.0],
[-0.5, 1.0, 0.5],
[-0.5, 2.0, 1.5],
[-0.5, -0.5, -0.0],
[-0.5, -1.0, -0.5],
[-0.5, -2.0, -0.5],
[-1.0, Infinity, Infinity],
[-1.0, -Infinity, -1.0],
[-1.0, NaN, NaN],
[-1.0, 0.0, NaN],
[-1.0, -0.0, NaN],
[-1.0, 0.5, 0.0],
[-1.0, 1.0, 0.0],
[-1.0, 2.0, 1.0],
[-1.0, -0.5, -0.0],
[-1.0, -1.0, -0.0],
[-1.0, -2.0, -1.0],
[-2.0, Infinity, Infinity],
[-2.0, -Infinity, -2.0],
[-2.0, NaN, NaN],
[-2.0, 0.0, NaN],
[-2.0, -0.0, NaN],
[-2.0, 0.5, 0.0],
[-2.0, 1.0, 0.0],
[-2.0, 2.0, 0.0],
[-2.0, -0.5, -0.0],
[-2.0, -1.0, -0.0],
[-2.0, -2.0, -0.0],
];

for (const [a, b, result] of cases) {
const actual = modulo(a, b);
assert(
Object.is(actual, result),
`modulo(${Deno.inspect(a)}, ${Deno.inspect(b)}) == ${
Deno.inspect(result)
} (actual ${Deno.inspect(actual)})`,
);
}
});
});
Loading
Loading