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 @@ -78,6 +78,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 @@ -30,6 +30,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
28 changes: 28 additions & 0 deletions math/clamp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// 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 min The minimum value
* @param max The maximum value
* @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);
* ```
*/
// deno-lint-ignore deno-style-guide/exported-function-args-maximum
export function clamp(num: number, min: number, max: number): number {
if (min > max) {
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";
31 changes: 31 additions & 0 deletions math/modulo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// 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 {
num %= modulus;
if (num === 0) {
num = modulus < 0 ? -0 : 0;
} else if ((num < 0) !== (modulus < 0)) {
num += modulus;
}
return num;
}
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