Search
⌘K

Math

Math utilities like lerp, clamp, etc.

Example Usage

import { 
  clamp, 
  lerp, 
  lerpInverse, 
  sum, 
  mean, 
  wrap,
  numIsBetween,
  calculateFrequenciesStats,
} from "./math"

// clamp
clamp({ min: 0, max: 100, value: 50 })  // 50
clamp({ min: 0, max: 100, value: 25 })  // 25
clamp({ min: 0, max: 100, value: 75 })  // 75
clamp({ min: 0, max: 100, value: 150 }) // 100
clamp({ min: 0, max: 100, value: -50 }) // 0

// lerp
// it automatically clamp value
lerp({ min: 0, max: 100, t: 0.5 })  // 50
lerp({ min: 0, max: 100, t: 0.25 }) // 25
lerp({ min: 0, max: 100, t: 0.75 }) // 75
lerp({ min: 0, max: 100, t: 1.5 })  // 100
lerp({ min: 0, max: 100, t: -0.5 }) // 0

// lerpInverse
// it automatically clamp value
lerpInverse({ min: 0, max: 100, value: 50 })  // 0.5
lerpInverse({ min: 0, max: 100, value: 25 })  // 0.25
lerpInverse({ min: 0, max: 100, value: 75 })  // 0.75
lerpInverse({ min: 0, max: 100, value: 150 }) // 1
lerpInverse({ min: 0, max: 100, value: -50 }) // 0

// sum
sum([]); // 0
sum([1, 2, 3]); // 6

// mean
mean([]); // 0
mean([1, 2, 3]); // 6 / 3 = 2
mean([1, 2, 3, 4]); // 10 / 4 = 2.5
mean([0, 0, 10]); // 3.3333333333333335

// wrap
wrap({ min: 0, max: 100, value: 200 }); // 0
wrap({ min: 0, max: 100, value: -10 }); // 100
wrap({ min: 0, max: 100, value: 34 }); // 34

// numIsBetween
numIsBetween({ min: 1, max: 10, num: 8, }); // 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: 1, isInclusive: false }); // false

// calculateFrequenciesStats
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

math.ts
/**
 * Source: http://localhost:3000
 */

/**
 * 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

math.test.ts
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...