Skip to content

Commit 98eb5e1

Browse files
committed
feat(math/unstable): add math package with basic math utilities
1 parent 91d1d55 commit 98eb5e1

15 files changed

+398
-0
lines changed

.github/labeler.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ json:
6767
jsonc:
6868
- changed-files:
6969
- any-glob-to-any-file: jsonc/**
70+
math:
71+
- changed-files:
72+
- any-glob-to-any-file: math/**
7073
media-types:
7174
- changed-files:
7275
- any-glob-to-any-file: media_types/**

.github/workflows/title.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ jobs:
6060
io(/unstable)?
6161
json(/unstable)?
6262
jsonc(/unstable)?
63+
math(/unstable)?
6364
media-types(/unstable)?
6465
msgpack(/unstable)?
6566
net(/unstable)?

browser-compat.tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"./io",
3333
"./json",
3434
"./jsonc",
35+
"./math",
3536
"./media_types",
3637
"./msgpack",
3738
"./net",

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"./io",
7777
"./json",
7878
"./jsonc",
79+
"./math",
7980
"./media_types",
8081
"./msgpack",
8182
"./net",

import_map.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@std/io": "jsr:@std/io@^0.225.2",
3232
"@std/json": "jsr:@std/json@^1.0.2",
3333
"@std/jsonc": "jsr:@std/jsonc@^1.0.2",
34+
"@std/math": "jsr:@std/math@^0.0.0",
3435
"@std/media-types": "jsr:@std/media-types@^1.1.0",
3536
"@std/msgpack": "jsr:@std/msgpack@^1.0.3",
3637
"@std/net": "jsr:@std/net@^1.0.6",

math/clamp.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
/**
5+
* Clamp a number within the inclusive [min, max] range.
6+
*
7+
* @param num The number to be clamped
8+
* @param limits The inclusive [min, max] range
9+
* @returns The clamped number
10+
*
11+
* @example Usage
12+
* ```ts
13+
* import { clamp } from "@std/math/clamp";
14+
* import { assertEquals } from "@std/assert";
15+
* assertEquals(clamp(5, [1, 10]), 5);
16+
* assertEquals(clamp(-5, [1, 10]), 1);
17+
* assertEquals(clamp(15, [1, 10]), 10);
18+
* ```
19+
*/
20+
export function clamp(num: number, limits: [min: number, max: number]): number {
21+
const [min, max] = limits;
22+
if (min > max) {
23+
throw new RangeError("`min` must be less than or equal to `max`");
24+
}
25+
26+
return Math.min(Math.max(num, min), max);
27+
}

math/clamp_test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
import { clamp } from "./clamp.ts";
3+
import { assert, assertEquals, assertThrows } from "@std/assert";
4+
5+
Deno.test("clamp()", async (t) => {
6+
await t.step("basic functionality", () => {
7+
assertEquals(clamp(5, [1, 10]), 5);
8+
assertEquals(clamp(-5, [1, 10]), 1);
9+
assertEquals(clamp(15, [1, 10]), 10);
10+
});
11+
12+
await t.step("NaN", () => {
13+
assertEquals(clamp(NaN, [0, 1]), NaN);
14+
assertEquals(clamp(0, [NaN, 0]), NaN);
15+
assertEquals(clamp(0, [0, NaN]), NaN);
16+
});
17+
18+
await t.step("infinities", () => {
19+
assertEquals(clamp(5, [0, Infinity]), 5);
20+
assertEquals(clamp(-5, [-Infinity, 10]), -5);
21+
});
22+
23+
await t.step("+/-0", () => {
24+
assert(Object.is(clamp(0, [0, 1]), 0));
25+
assert(Object.is(clamp(-0, [-0, 1]), -0));
26+
27+
assert(Object.is(clamp(-2, [0, 1]), 0));
28+
assert(Object.is(clamp(-2, [-0, 1]), -0));
29+
assert(Object.is(clamp(2, [-1, 0]), 0));
30+
assert(Object.is(clamp(2, [-1, -0]), -0));
31+
32+
assert(Object.is(clamp(-0, [0, 1]), 0));
33+
assert(Object.is(clamp(0, [-0, 1]), 0));
34+
35+
assert(Object.is(clamp(-0, [-1, 0]), -0));
36+
assert(Object.is(clamp(0, [-1, -0]), -0));
37+
});
38+
39+
await t.step("errors", () => {
40+
assertThrows(
41+
() => clamp(5, [10, 1]),
42+
RangeError,
43+
"`min` must be less than or equal to `max`",
44+
);
45+
});
46+
});

math/deno.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "@std/math",
3+
"version": "0.0.0",
4+
"exports": {
5+
".": "./mod.ts",
6+
"./clamp": "./clamp.ts",
7+
"./integer-range": "./integer_range.ts",
8+
"./modulo": "./modulo.ts",
9+
"./round-to": "./round_to.ts"
10+
}
11+
}

math/integer_range.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
/**
5+
* Options for {@linkcode integerRange}.
6+
*/
7+
export type IntegerRangeOptions = {
8+
/**
9+
* The step between each number in the range.
10+
* @default {1}
11+
*/
12+
step?: number;
13+
/**
14+
* Whether to include the start value in the range.
15+
* @default {true}
16+
*/
17+
includeStart?: boolean;
18+
/**
19+
* Whether to include the end value in the range.
20+
* @default {false}
21+
*/
22+
includeEnd?: boolean;
23+
};
24+
25+
/**
26+
* Creates a generator that yields integers in a range from `start` to `end`.
27+
*
28+
* Using the default options, yielded numbers are in the interval `[start, end)` with step size `1`.
29+
*
30+
* @example Usage
31+
* ```ts
32+
* import { integerRange } from "@std/math/integer-range";
33+
* import { assertEquals } from "@std/assert";
34+
* assertEquals([...integerRange(1, 5)], [1, 2, 3, 4]);
35+
* assertEquals([...integerRange(1, 5, { step: 2 })], [1, 3]);
36+
* assertEquals(
37+
* [...integerRange(1, 5, { includeStart: false, includeEnd: true })],
38+
* [2, 3, 4, 5],
39+
* );
40+
* assertEquals([...integerRange(5, 1)], []);
41+
* assertEquals([...integerRange(5, 1, { step: -1 })], [5, 4, 3, 2]);
42+
* ```
43+
*/
44+
export function* integerRange(
45+
start: number,
46+
end: number,
47+
options?: IntegerRangeOptions,
48+
): Generator<number, undefined, undefined> {
49+
const { step = 1, includeStart = true, includeEnd = false } = options ?? {};
50+
if (step === 0) throw new RangeError("`step` must not be zero");
51+
for (const [k, v] of Object.entries({ start, end, step })) {
52+
if (!Number.isSafeInteger(v)) {
53+
throw new RangeError(`\`${k}\` must be a safe integer`);
54+
}
55+
}
56+
57+
const limitsSign = Math.sign(end - start);
58+
const stepSign = Math.sign(step);
59+
if (limitsSign !== 0 && limitsSign !== stepSign) return;
60+
61+
if (includeStart) yield start;
62+
if (start > end) { for (let i = start + step; i > end; i += step) yield i; }
63+
else for (let i = start + step; i < end; i += step) yield i;
64+
if (includeEnd && (start !== end || !includeStart)) yield end;
65+
}

math/integer_range_test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2018-2025 the Deno authors. MIT license.
2+
import { integerRange } from "@std/math/integer-range";
3+
import { assertEquals, assertThrows } from "@std/assert";
4+
5+
Deno.test("integerRange()", async (t) => {
6+
await t.step("basic", () => {
7+
const range = integerRange(1, 5);
8+
assertEquals([...range], [1, 2, 3, 4]);
9+
// already consumed iterator
10+
assertEquals([...range], []);
11+
});
12+
13+
await t.step("`step`", () => {
14+
assertEquals([...integerRange(1, 5, { step: 2 })], [1, 3]);
15+
});
16+
17+
await t.step("`includeStart`, `includeEnd`", () => {
18+
assertEquals(
19+
[...integerRange(1, 5, { includeStart: false, includeEnd: true })],
20+
[2, 3, 4, 5],
21+
);
22+
});
23+
24+
await t.step(
25+
"`start` and `end` in opposite order to `step` yield no results",
26+
() => {
27+
assertEquals([...integerRange(5, 1)], []);
28+
assertEquals([...integerRange(1, 5, { step: -1 })], []);
29+
},
30+
);
31+
32+
await t.step("decreasing range with negative step", () => {
33+
assertEquals([...integerRange(5, 1, { step: -1 })], [5, 4, 3, 2]);
34+
});
35+
36+
await t.step("`start` == `end`", () => {
37+
assertEquals([...integerRange(0, 0)], [0]);
38+
assertEquals([
39+
...integerRange(0, 0, { includeStart: true, includeEnd: true }),
40+
], [0]);
41+
assertEquals([
42+
...integerRange(0, 0, { includeStart: false, includeEnd: true }),
43+
], [0]);
44+
45+
// if _both_ are false, nothing is yielded
46+
assertEquals([
47+
...integerRange(0, 0, { includeStart: false, includeEnd: false }),
48+
], []);
49+
});
50+
51+
await t.step("errors", () => {
52+
assertThrows(
53+
() => [...integerRange(1.5, 5)],
54+
RangeError,
55+
"`start` must be a safe integer",
56+
);
57+
assertThrows(
58+
() => [...integerRange(1, 5, { step: 1.5 })],
59+
RangeError,
60+
"`step` must be a safe integer",
61+
);
62+
assertThrows(
63+
() => [...integerRange(1, 5.5)],
64+
RangeError,
65+
"`end` must be a safe integer",
66+
);
67+
assertThrows(
68+
() => [...integerRange(1, 5, { step: 0 })],
69+
RangeError,
70+
"`step` must not be zero",
71+
);
72+
});
73+
});

0 commit comments

Comments
 (0)