How to generate Number Range Array in TypeScript

eye-catch JavaScript/TypeScript

JavaScript/TypeScript doesn’t offer a function to generate a range array. What we want to do is something like this.

Array.range(5, 10); // [5, 6, 7, 8, 9, 10]
Array.range(2, 10, 2); // [2, 4, 6, 8, 10]

How can we write code for this?

I created a module called yutolity that provides the functions. Go to npm and add it to your dependencies if you don’t want to write code.

yutolity
Utility functions. Latest version: 1.4.0, last published: 2 years ago. Start using yutolity in your project by running `...
Sponsored links

Integer value Array Range

Ascending order array

If we want to generate an integer array like [1, 2, 3, 4] we can generate it with this one line.

const res = Array.from(Array(5).keys()).map(x => x + 1);
console.log(res);
// [1, 2, 3, 4, 5]

If we want to start the array from 5 we need to change x + 1 to x + 5;

const res = Array.from(Array(5).keys()).map(x => x + 5);
console.log(res);
// [1, 2, 3, 4, 5]

Let’s make it to a function.

const range = (start, end) => Array.from(Array(end - start + 1).keys()).map(x => x + start);
console.log(range(3, 6));
// [3, 4, 5, 6]
console.log(range(-2, 2));
// [-2, -1, 0, 1]

Descending order array

The code shown above doesn’t work if the end value is smaller than the start. Let’s improve it.

export function range(start: number, end: number): number[] {
    start = Math.floor(start);
    end = Math.floor(end);

    const diff = end - start;
    if (diff === 0) {
        return [start];
    }

    const keys = Array(Math.abs(diff) + 1).keys();
    return Array.from(keys).map(x => {
        const increment = end > start ? x : -x;
        return start + increment;
    });
}

The last 5 lines are the main logic. It looks more complicated but it’s not hard. It just decides if it increments or decrements.

Sponsored links

Floating value Array Range

If we want to generate a floating value array we need to implement it in a different way. Following is the first code that I wrote.

export function rangeByStep(start: number, end: number, step: number): number[] {
    if (end === start || step === 0) {
        return [start];
    }
    if (step < 0) {
        step = -step;
    }
    const stepNumOfDecimal = step.toString().split(".")[1]?.length || 0;
    const endNumOfDecimal = end.toString().split(".")[1]?.length || 0;

    const maxNumOfDecimal = Math.max(stepNumOfDecimal, endNumOfDecimal);
    const power = Math.pow(10, maxNumOfDecimal);
    const increment = end - start > 0 ? step : -step;
    const intEnd = Math.floor(end * power);

    const isFulFilled = end - start > 0 ?
        (current: number) => current > intEnd:
        (current: number) => current < intEnd

    const result = [];
    let current = start;
    while (true) {
        result.push(current);
        // to address floating value
        const intValue = Math.floor(current * power) + Math.floor(increment * power);
        current = intValue / power;
        if (isFulFilled(intValue)) {
            break;
        }
    }
    return result;
}

It’s much more complicated than the one for the integer version because it’s not so simple to handle floating values. When repeating to add 0.1 to the existing variable its result is 0.200000001 for example. Therefore, I converted the floating values to integers and then started calculations to prevent this. Furthermore, Math.floor returns -31 if the argument is like -30.0000001. I needed to replace the function with Math.trunc.
I thought that I could remove the value comparison if I wrote it in the same way as the integer version. The second version is the following.

export function rangeByStep(start: number, end: number, step: number): number[] {
    if (end === start || step === 0) {
        return [start];
    }
    if (step < 0) {
        step = -step;
    }

    const stepNumOfDecimal = step.toString().split(".")[1]?.length || 0;
    const endNumOfDecimal = end.toString().split(".")[1]?.length || 0;
    const maxNumOfDecimal = Math.max(stepNumOfDecimal, endNumOfDecimal);
    const power = Math.pow(10, maxNumOfDecimal);
    const diff = Math.abs(end - start);
    const count = Math.trunc(diff / step + 1);
    step = end - start > 0 ? step : -step;

    const intStart = Math.trunc(start * power);
    return Array.from(Array(count).keys())
        .map(x => {
            const increment = Math.trunc(x * step * power);
            const value = intStart + increment;
            return Math.trunc(value) / power;
        });
}

The following code gets the length of the number after the decimal point.

const stepNumOfDecimal = step.toString().split(".")[1]?.length || 0;
const endNumOfDecimal = end.toString().split(".")[1]?.length || 0;
// 11 -> 0
// 11.123 -> 3
// 123.12345 -> 5

This function can do the same thing that I wrote at the top of this article.

Array.range(2, 10, 2); // [2, 4, 6, 8, 10]
rangeByStep(2, 10, 2); // [2, 4, 6, 8, 10]

Handle Big/Small Number with Log Notation

However, it can’t cover some cases when the number is very big or small because those numbers can be converted to log notation.

console.log(0.000000001);
// 1e-9

My function checks the length after the decimal point but it doesn’t work without the dot “.”. The logic needs to be updated. The improved code is the following.

export function indexesOf(
    text: string, 
    searchStrings: string[], 
    position?: number
): IndexOfResult | undefined {
    for (const searchString of searchStrings) {
        const index = text.indexOf(searchString, position);
        if (index !== -1) {
            return {
                index,
                foundString: searchString,
            };
        }
    }
}

export function getPrecision(value: number): number {
    const str = value.toString();
    const searchStrings = ["e-", "e+"];
    const found = indexesOf(str, searchStrings);
    if (found) {
        return parseInt(str.substring(found.index + 2));
    }
    return str.split(".")[1]?.length || 0;
}

getPrecision(11.223); // 3
getPrecision(11.0);   // 0
getPrecision(1e-11);  // 11
getPrecision(1e+21);  // 21

Even if it receives a log notation value it can get the precision. If we replace it with the previous version rangeByStep function supports all most all cases. I’m sure this is probably too much. In most cases, such a big/small number isn’t used.

The final version is the following.

export function rangeByStep(start: number, end: number, step: number): number[] {
    if (end === start || step === 0) {
        return [start];
    }
    if (step < 0) {
        step = -step;
    }

    const stepPrecision = getPrecision(step);
    const endPrecision = getPrecision(end);
    const maxPrecision = Math.max(stepPrecision, endPrecision);
    const power = Math.pow(10, maxPrecision);
    const intStart = Math.round(start * power);
    const intEnd = Math.round(end * power);
    const diff = Math.abs(intEnd - intStart);
    const count = Math.trunc(diff / Math.round(step * power) + 1);
    step = end - start > 0 ? step : -step;

    return Array.from(Array(count).keys())
        .map(x => {
            const increment = Math.round(x * step * power);
            const value = intStart + increment;
            return value / power;
        });
}
rangeByStep(1, 7, 2);         // [1, 3, 5, 7]
rangeByStep(0.1, 0.3, -0.1);  // [0.1, 0.2, 0.3]
rangeByStep(0.3, -0.11, 0.1); // [0.3, 0.2, 0.1, 0, -0.1]
rangeByStep(-0.2, 0.3, 1);    // [-0.2]
rangeByStep(0.000001, 0.000003, 0.000001); // [0.000001, 0.000002, 0.000003]

End

The logic to generate an integer range array is easy but not for a floating value. We need to consider calculation errors and log notation. It was a cumbersome calculation. I guess we use a normal integer range array in most cases so you can easily write the code yourself. Copy my code and paste it.

If you want to use the utility functions shown in this article you can add my module yutolity.

Related articles

Comments

Copied title and URL