Math
Math utilities like lerp, clamp...
Example Usage
clamp
Keep a number within a minimum and maximum range — if it’s too low or too high, it’s pushed back inside the limits.
The mental model is:
- if the value is lower than min, min is returned
- if the value is greater than max, max is returned
import { clamp } from "./math"
// inside range
clamp({ min: 0, max: 100, value: 25 }) // 25
clamp({ min: 0, max: 100, value: 50 }) // 50
clamp({ min: 0, max: 100, value: 75 }) // 75
// outside range
clamp({ min: 0, max: 100, value: -50 }) // 0
clamp({ min: 0, max: 100, value: 150 }) // 100clamp and wrap can be confused, but clamp return the surpassed edge, while wrap return the opposite edge.
clamp({ min: 0, max: 100, value: 200 }); // 100
wrap({ min: 0, max: 100, value: 200 }); // 0wrap
Keep a number inside a range by looping it around — if it goes past the max, it starts again from the min.
The mental model is:
- if the value is lower than min, max is returned
- if the value is greater than max, min is returned
import { wrap } from "./math"
// inside min-max range
wrap({ min: 0, max: 100, value: 25 }) // 25
wrap({ min: 0, max: 100, value: 50 }) // 50
wrap({ min: 0, max: 100, value: 75 }) // 75
// outside min-max range
wrap({ min: 0, max: 100, value: 200 }); // 0
wrap({ min: 0, max: 100, value: -10 }); // 100clamp and wrap can be confused, but clamp return the surpassed edge, while wrap return the opposite edge.
clamp({ min: 0, max: 100, value: 200 }); // 100
wrap({ min: 0, max: 100, value: 200 }); // 0lerp
Find a value between two numbers based on a progress value from 0 to 1 — for example, halfway (0.5) gives the middle point.
This is the inverse of lerpInverse.
import { lerp } from "./math"
// NOTE: it automatically clamp value
// inside range (t is between 0 and 1)
lerp({ min: 0, max: 100, t: 0.25 }) // 25
lerp({ min: 0, max: 100, t: 0.5 }) // 50
lerp({ min: 0, max: 100, t: 0.75 }) // 75
// outside range (t is lesser than 0 or greater than 1)
lerp({ min: 0, max: 100, t: -0.5 }) // 0
lerp({ min: 0, max: 100, t: 1.5 }) // 100lerpInverse
Find how far a number is between two limits — returns a value between 0 and 1 representing its position within the range.
This is the inverse of lerp.
import { lerpInverse } from "./math"
// NOTE: it automatically clamp value
// inside min-max range
lerpInverse({ min: 0, max: 100, value: 25 }) // 0.25
lerpInverse({ min: 0, max: 100, value: 50 }) // 0.5
lerpInverse({ min: 0, max: 100, value: 75 }) // 0.75
// outside min-max range
lerpInverse({ min: 0, max: 100, value: -50 }) // 0
lerpInverse({ min: 0, max: 100, value: 150 }) // 1sum
Add together all the numbers in an array — returns 0 if the list is empty.
import { sum } from "./math"
sum([]); // 0
sum([1, 2, 3]); // 6mean
Calculate the average of a list of numbers — the result is the total divided by how many numbers there are.
import { mean } from "./math"
// no items
mean([]); // 0
// with items
mean([1, 2, 3]); // 6 / 3 = 2
mean([1, 2, 3, 4]); // 10 / 4 = 2.5
// what does 0 do?
mean([0, 0, 10]); // 3.3333333333333335numIsBetween
Check whether a number falls inside a given range, with the option to include or exclude the boundaries.
import { numIsBetween } from "./math"
// isInclusive=true
numIsBetween({ min: 1, max: 10, num: 1, isInclusive: true }); // true
numIsBetween({ min: 1, max: 10, num: 1, }); // true (isInclusive defaults to true)
numIsBetween({ min: 1, max: 10, num: 5, }); // true
numIsBetween({ min: 1, max: 10, num: 10, }); // true
// isInclusive=false
numIsBetween({ min: 1, max: 10, num: 1, isInclusive: false }); // false
numIsBetween({ min: 1, max: 10, num: 5, isInclusive: false }); // true
numIsBetween({ min: 1, max: 10, num: 10, isInclusive: false }); // falsecalculateFrequenciesStats
Convert a set of counts into total and percentage values for each group, making it easy to compare proportions.
import { calculateFrequenciesStats } from "./math"
calculateFrequenciesStats({ A: 4, B: 6 }));
// ⏬
{
total: 10,
groups: {
A: { count: 4, percentageOnTotal: 0.4 },
B: { count: 6, percentageOnTotal: 0.6 },
}
}
calculateFrequenciesStats({ A: 1, B: 2 }));
// ⏬
{
total: 3,
groups: {
A: { count: 1, percentageOnTotal: 0.3333333333333333 },
B: { count: 2, percentageOnTotal: 0.6666666666666666 },
}
};
Dependencies
No dependencies
Auto Install
npx shadcn@latest add https://shadcn-registry-ts.vercel.app/r/util-math.json
Manual Install
/**
* Clamp function, constraints a value to be in a range.
* Outliers will be clamped to the relevant extreme of the range.
*/
export function clamp({ min, max, value }: {
/** Minimin possibile value. */
min: number,
/** Maximinum possible value. */
max: number,
/** Value you want to clamp */
value: number;
}) {
if (value < min) return min;
if (value > max) return max;
return value;
}
/**
* Lerp function, used to get a value in range based on a percentage.
* Outliers will be clamped.
*/
export function lerp({ min, max, t }: {
/** Lower part of the a-b range. Minumum value passibile. */
min: number,
/** Upper part of the a-b range. Maximum value possible. */
max: number,
/** Number, decimal, from 0.0 to 1.0, which rapresent where value lives between a-b range. */
t: number;
}) {
const value = (max - min) * t + min;
return clamp({ min, max, value });
}
/**
* Lerp Inversed function, used to get the percentage of a value in a range.
* Outliers will be clamped.
*/
export function lerpInverse({ min, max, value }: {
/** Lower part of the a-b range. Minumum value passibile. */
min: number,
/** Upper part of the a-b range. Maximum value possible. */
max: number,
/** Number that must be in range a-b, rapresent the value that you want to know where it sits in a-b range. */
value: number;
}): number {
const t = (value - min) / (max - min);
return clamp({ min: 0, max: 1, value: t });
}
/**
* Sum function. Accept array of numbers and return the sum.
*/
export const sum = (
/** Array of numbers */
nums: number[]
): number => {
return nums.reduce((acc, num) => acc + num, 0);
};
/**
* Mean function. Accept array of numbers and return the mean.
*/
export function mean(
/** Array of numbers */
nums: number[]
): number {
if (nums.length === 0) return 0;
return sum(nums) / nums.length;
}
/**
* Wrap value in range [min, max].
* If value is greater than max, min is returned.
* If value is lower than min, max is returned.
* If value is in between min and max, value is returned.
*/
export function wrap({ min, max, value }: {
/** Lower part of the range. */
min: number,
/** Upper part of the range. */
max: number,
/** Value to wrap. */
value: number;
}) {
if (value < min) return max;
if (value > max) return min;
return value;
}
/**
* Check if a number is between a range.
*/
export const numIsBetween = ({
min,
max,
num,
isInclusive = true,
}: {
/** Lower part of the range. */
min: number,
/** Upper part of the range. */
max: number,
/** Number to check. */
num: number,
/** If `true` min and max are allowed values, if `false` min and max are not allowed values. Deafult: `true` */
isInclusive?: boolean,
}) => {
if (isInclusive) {
return num >= min && num <= max;
}
return num > min && num < max;
};
/**
* Calculate percentages of an object that represents frequencies.
* @example
* ```ts
* const frequencies = calculateFrequenciesStats({ A: 4, B: 6 });
* console.log(frequencies);
* // output
* {
* total: 10,
* groups: {
* A: { count: 4, percentageOnTotal: 0.4 },
* B: { count: 6, percentageOnTotal: 0.6 },
* }
* }
* ```
*/
export const calculateFrequenciesStats = (calculations: { [k: string]: number; }) => {
type Data = {
/** total executions of all groups */
total: number,
/** stats for each group */
groups: Record<string, {
/** times this group was calculated */
count: number;
/** percentage of times this group was calculated against total executions of all groups. 0-1 range. */
percentageOnTotal: number;
}>;
};
const total = sum(Object.values(calculations));
const data: Data = {
total,
groups: Object.fromEntries(
Object.entries(calculations).map(([calcKey, calcCount]) => {
const percentage = (calcCount / total);
const data = {
count: calcCount,
percentageOnTotal: percentage,
};
return [calcKey, data];
}))
};
return data;
};Test
import { describe, it, expect } from 'vitest';
import {
clamp,
lerp,
lerpInverse,
sum,
mean,
wrap,
numIsBetween,
calculateFrequenciesStats,
} from './math';
describe('math - clamp', () => {
it('do it', () => {
// with value inside range
expect(clamp({ min: 0, max: 100, value: 50 })).toBe(50);
expect(clamp({ min: 0, max: 100, value: 25 })).toBe(25);
expect(clamp({ min: 0, max: 100, value: 75 })).toBe(75);
// with value outside range
expect(clamp({ min: 0, max: 100, value: -50 })).toBe(0);
expect(clamp({ min: 0, max: 100, value: 150 })).toBe(100);
});
});
describe('math - lerp', () => {
it('do it', () => {
// with t inside 0-1 range
expect(lerp({ min: 0, max: 100, t: 0.5 })).toBe(50);
expect(lerp({ min: 0, max: 100, t: 0.25 })).toBe(25);
expect(lerp({ min: 0, max: 100, t: 0.75 })).toBe(75);
// with t outise 0-1 range
expect(lerp({ min: 0, max: 100, t: -0.5 })).toBe(0);
expect(lerp({ min: 0, max: 100, t: 1.5 })).toBe(100);
});
});
describe('math - lerpInverse', () => {
it('do it', () => {
// with value inside range
expect(lerpInverse({ min: 0, max: 100, value: 50 })).toBe(0.5);
expect(lerpInverse({ min: 0, max: 100, value: 25 })).toBe(0.25);
expect(lerpInverse({ min: 0, max: 100, value: 75 })).toBe(0.75);
// with value outside range
expect(lerpInverse({ min: 0, max: 100, value: -50 })).toBe(0);
expect(lerpInverse({ min: 0, max: 100, value: 150 })).toBe(1);
});
});
describe('math - sum', () => {
it('do it', () => {
expect(sum([])).toBe(0);
expect(sum([1, 2, 3])).toBe(6);
});
});
describe('math - mean', () => {
it('do it', () => {
expect(mean([])).toBe(0);
expect(mean([1, 2, 3])).toBe(2);
expect(mean([1, 2, 3, 4])).toBe(2.5);
expect(mean([0, 0, 10])).toBe(3.3333333333333335);
});
});
describe('math - wrap', () => {
it('do it', () => {
// with value inside range
expect(wrap({ min: 0, max: 100, value: 34 })).toBe(34);
// with value outside range
expect(wrap({ min: 0, max: 100, value: -10 })).toBe(100);
expect(wrap({ min: 0, max: 100, value: 200 })).toBe(0);
});
});
describe('math - numIsBetween', () => {
it('do it', () => {
// with value inside range
expect(numIsBetween({ min: 0, max: 100, num: 0 })).toBe(true);
expect(numIsBetween({ min: 0, max: 100, num: 50 })).toBe(true);
expect(numIsBetween({ min: 0, max: 100, num: 100 })).toBe(true);
expect(numIsBetween({ min: 0, max: 100, num: 0, isInclusive: true })).toBe(true);
expect(numIsBetween({ min: 0, max: 100, num: 50, isInclusive: true })).toBe(true);
expect(numIsBetween({ min: 0, max: 100, num: 100, isInclusive: true })).toBe(true);
expect(numIsBetween({ min: 0, max: 100, num: 0, isInclusive: false })).toBe(false);
expect(numIsBetween({ min: 0, max: 100, num: 50, isInclusive: false })).toBe(true);
expect(numIsBetween({ min: 0, max: 100, num: 100, isInclusive: false })).toBe(false);
// with value outside range
expect(numIsBetween({ min: 0, max: 100, num: -1 })).toBe(false);
expect(numIsBetween({ min: 0, max: 100, num: 101 })).toBe(false);
});
});
describe('math - calculateFrequenciesStats', () => {
it('do it', () => {
expect(calculateFrequenciesStats({ A: 4, B: 6 })).toMatchObject({
total: 10,
groups: {
A: { count: 4, percentageOnTotal: 0.4 },
B: { count: 6, percentageOnTotal: 0.6 },
}
});
expect(calculateFrequenciesStats({ A: 1, B: 2 })).toMatchObject({
total: 3,
groups: {
A: { count: 1, percentageOnTotal: 0.3333333333333333 },
B: { count: 2, percentageOnTotal: 0.6666666666666666 },
}
});
});
});
Command Palette
Search for a command to run...