one day of code

Range to range translation

July 07, 2023

Have you ever needed to translate one range to the another? Usually we have a range 0 - 100. Then we do all calculations for that range. But what if we need the range narrowed?

Let’s say you have a range of values (700 - 15000) that you need to map to l value of hsl which are 0 - 100. But, 0 is black and 100 is white. What about, let’s say 10-90?

Here are the requirements. We want to:

  • have some values in range from i.e. 372 (min) and 1427 (max)
  • map min value to 10% of l and max value to 90% of l
  • maybe optionally have stepped increment instead of direct (continous)

Let’s explore this topic:

  • We need the user to provide several values:
    • minimum, maximum and target input values
    • minimum and maximum output value with maybe step size

As a result, we’ll provide appropriate output value within output range for given input value.

PS: Don’t mind the snippet format, this only demonstrates the math. You can (re)write it to your own implementation (class or whatever)

// Define the format of our arguments
interface TranslatorOptions {
minInput: number;
maxInput: number;
lowOutput: number;
highOutput: number;
steps: number;
}
// Set the default argument values
const defaults: TranslatorOptions = {
minInput: 111,
maxInput: 531,
lowOutput: 10,
highOutput: 90,
steps: 5,
}
/**
* Translate one range to another
* @param value { number } Value in range of `options.minInput`
* and `options.maxInput`
* @param options [defaults] { TranslatorOptions }
*/
function translate(
value: number,
options: Partial<TranslatorOptions> = defaults
) {
const mergedOptions = {
...defaults,
...options } as TranslatorOptions;
// Calculate the the "available" difference between extreme values
const range = mergedOptions.highOutput - mergedOptions.lowOutput;
/**
* Set the "inversed" steps. Best to explain by example:
* We define 5 steps, i.e. 1, 2, 3, 4, 5. This means that
* if we have max value of 100, we need to divide by 20.
* Otherwise we'd have 20 steps...
*/
const step = mergedOptions.steps ? range / mergedOptions.steps : 0;
/**
* Subtract minInput value from provided `value` and `maxInput`
* to reset calculation to zero, so we can get correct
* math result.
*/
const valuePercentage = Math.round(
(
(value - mergedOptions.minInput) /
(mergedOptions.maxInput - mergedOptions.minInput)
) * range
);
/**
* Here we get the final result ranging from `lowOutput` to
* `highOutput` for given range of `minInput`/`maxInput`
*/
const lightness = valuePercentage + mergedOptions.lowOutput;
// And last, we add stepping for provided `value` if correct
return step
? mergedOptions.lowOutput + Math.floor(step * Math.floor(lightness / step))
: lightness;
}

Here are some examples with the defaults from above:

translate(231, { steps: 3 })
// 36
translate(423, { steps: 10 })
// 74
translate(423)
// 69

Written by Milan Miljkovic — a tech enthusiast and design system practitioner.