Skip to content

Instantly share code, notes, and snippets.

@joshwashywash
Last active March 31, 2023 23:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joshwashywash/88bc0da70eb7783d643a7f5f8f6ba184 to your computer and use it in GitHub Desktop.
Save joshwashywash/88bc0da70eb7783d643a7f5f8f6ba184 to your computer and use it in GitHub Desktop.
map from one range to another

This gist isn't mathematically rigorous so don't expect it to be as formal as something you'd find in academia.

how do you map from one range of numbers to another?

This problem arises in various math and computer science topics where your input range needs to be transformed to fit into a different range. For example, let's say you had frequency data of a song at a particular moment in time. Each sample is a value that ranges from 0 to 255. In interval notation, it would be written as [0, 255]. The square brackets denote that the endpoints are inclusive - in other words, 0 and 255 are acceptable. We want to take a sample in this range and map it to a different range. Let's say our output range is [-1, 1]. The [-1, 1] range actually pops up a lot in various fields such as graphics programming or trigonometry. For example the range for the sine and cosine functions is [-1, 1].

So we want to take the range [0, 255] and map it to the range [-1, 1]. Of course, we'd want the mapping to make sense where the lowest value in the input range maps to the lowest value in the output range, 0 -> -1 in this case. Likewise, the highest value in the input range, 255 should map to 1 and everything in between maps linearly. So how do we do this? It's actually a simple transformation that involves scaling and translating. To formally state the goal, we want to take a range [a, b] and map it to a range [c, d].

step 1: translate to the origin

First we want to shift the left value to 0. We can do this by shifting the range by a

A(a, t) -> t - a

const A = (a: number) => (t: number): number => t - a;

This will slide the range of values to begin at the origin and we'll end up with the range [a - a, b - a] or [0 - a, b - a]. For the example, [0 - 0, 255 - 0] or [0, 255]. In this case there was no change because the input range already conveniently started at the origin. Even though we could have skipped this step, it's not always skippable and it will account for any input ranges where a =/= 0.

step 2: normalize

Next we want to scale the input range to unit length, i.e. the range: [0, 1]. We can do this by dividing by the difference from the highest value in the input range to the lowest, b - a or in this case 255 - 0.

B(a, b, t) -> t / (b - a)

const B = (a: number) => (b: number) => (t: number): number => t / (b - a)

This means that 0 will stay at 0 because 0 / (a - b) = 0 (Yes, this means a and b must be different because if they were the same, b - a = 0 and you'd get division by zero.) The highest value in the range would get mapped to 1 because (b - a) / (b - a) = 1. Remember that in the first step we subtracted by a. So with our example, [0 / (b - a), 255 / (b - a)] becomes [0 / 255, 255 / 255] or [0, 1].

step 3: scale to output range

Now we're currently at the range [0, 1]. To get to [c, d] we kinda do the inverse of steps 1 and 2 in reverse order. First we'll scale by d - c or the difference between the highest and lowest values in the output range.

C(c, d, t) -> t * (d - c)

const C = (c: number) => (d: number) => (t: number): number => t * (d - c);

Using our example, d - c = 1 - (-1) = 2. Therefore C applied to [0, 1] gives [0, 2].

step 4: translate to the start of the output range

After this we'll have the range [0, d - c]. All we need to do now is add c.

D(c, t) -> t + c

const D = (c: number) => (t: number): number => t + c;

This last step gets us from [0, d - c] to [c, d]. Using c = -1 from our example, D applied to [0, 2] -> [0 + (-1), 2 + (-1)] -> [-1, 1]. We've done it! We started with the range [a, b] or [0, 255] and made our way to the range [c, d] or [-1, 1].

What's really nice is that we can compose all of these transformations into a single function. To start, we'll substitute A into B as follows:

A(a, t) -> t - a
B(a, b, t) -> t / (b - a)

B(A) -> B(t - a) -> (t - a) / (b - a) = E(a, b, t)

We can then substitute E into C:

C(t, c, d) -> t * (d - c)

C(E) -> C((t - a) / (b - a)) -> (t - a) / (b - a) * (d - c) = F(a, b, c, d, t)

And lastly we can substitute F into D:

D(c, t) -> t + c

D(F) -> D((t - a) / (b - a) * (d - c)) -> (t - a) / (b - a) * (d - c) + c = G(a, b, c, d, t)

G is our final transformation function. It takes a value from the input range [a, b] and maps it to a value in the output range [c, d].

In TypeScript this would look like:

const G =
	(a: number, b: number, c: number, d: number) =>
	(t: number): number =>
		((t - a) / (b - a)) * (d - c) + c;


const a = 0;
const b = 255;
const halfway = (a + b) / 2;
const c = -1;
const d = 1;
const m = G(a, b, c, d);

const ts = [a, halfway, b];
console.log(ts.map(m)); // [-1, 0, 1]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment