36

In JavaScript, how can I convert a sequence of numbers in an array to a range of numbers? In other words, I want to express consecutive occurring integers (no gaps) as hyphenated ranges.

[2,3,4,5,10,18,19,20] would become [2-5,10,18-20]

[1,6,7,9,10,12] would become [1,6-7,9-10,12]

[3,5,99] would remain [3,5,99]

[5,6,7,8,9,10,11] would become [5-11]

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
gokul
  • 361
  • 1
  • 3
  • 3

13 Answers13

38

Here is an algorithm that I made some time ago, originally written for C#, now I ported it to JavaScript:

function getRanges(array) {
  var ranges = [], rstart, rend;
  for (var i = 0; i < array.length; i++) {
    rstart = array[i];
    rend = rstart;
    while (array[i + 1] - array[i] == 1) {
      rend = array[i + 1]; // increment the index if the numbers sequential
      i++;
    }
    ranges.push(rstart == rend ? rstart+'' : rstart + '-' + rend);
  }
  return ranges;
}

getRanges([2,3,4,5,10,18,19,20]);
// returns ["2-5", "10", "18-20"]
getRanges([1,2,3,5,7,9,10,11,12,14 ]);
// returns ["1-3", "5", "7", "9-12", "14"]
getRanges([1,2,3,4,5,6,7,8,9,10])
// returns ["1-10"]
Community
  • 1
  • 1
Christian C. Salvadó
  • 807,428
  • 183
  • 922
  • 838
5

Just having fun with solution from CMS :

  function getRanges (array) {
    for (var ranges = [], rend, i = 0; i < array.length;) {
      ranges.push ((rend = array[i]) + ((function (rstart) {
        while (++rend === array[++i]);
        return --rend === rstart;
      })(rend) ? '' : '-' + rend)); 
    }
    return ranges;
  }
HBP
  • 15,685
  • 6
  • 28
  • 34
3

Very nice question: here's my attempt:

function ranges(numbers){
    var sorted = numbers.sort(function(a,b){return a-b;});
    var first = sorted.shift();
    return sorted.reduce(function(ranges, num){
        if(num - ranges[0][1] <= 1){
            ranges[0][1] = num;        
        } else {
            ranges.unshift([num,num]);
        }
        return ranges;
    },[[first,first]]).map(function(ranges){
        return ranges[0] === ranges[1] ? 
            ranges[0].toString() : ranges.join('-');
    }).reverse();
}

Demo on JSFiddler

Sandro Paganotti
  • 2,295
  • 16
  • 12
3

I needed TypeScript code today to solve this very problem -- many years after the OP -- and decided to try a version written in a style more functional than the other answers here. Of course, only the parameter and return type annotations distinguish this code from standard ES6 JavaScript.

  function toRanges(values: number[],
                    separator = '\u2013'): string[] {
    return values
      .slice()
      .sort((p, q) => p - q)
      .reduce((acc, cur, idx, src) => {
          if ((idx > 0) && ((cur - src[idx - 1]) === 1))
            acc[acc.length - 1][1] = cur;
          else acc.push([cur]);
          return acc;
        }, [])
      .map(range => range.join(separator));
  }

Note that slice is necessary because sort sorts in place and we can't change the original array.

Mark Florence
  • 363
  • 4
  • 8
1

Here's my take on this...

function getRanges(input) {

  //setup the return value
  var ret = [], ary, first, last;

  //copy and sort
  var ary = input.concat([]);
  ary.sort(function(a,b){
    return Number(a) - Number(b);
  });

  //iterate through the array
  for (var i=0; i<ary.length; i++) {
    //set the first and last value, to the current iteration
    first = last = ary[i];

    //while within the range, increment
    while (ary[i+1] == last+1) {
      last++;
      i++;
    }

    //push the current set into the return value
    ret.push(first == last ? first : first + "-" + last);
  }

  //return the response array.
  return ret;
}
Tracker1
  • 19,103
  • 12
  • 80
  • 106
1

Using ES6, a solution is:

function display ( vector ) { // assume vector sorted in increasing order
    // display e.g.vector [ 2,4,5,6,9,11,12,13,15 ] as "2;4-6;9;11-13;15"
    const l = vector.length - 1; // last valid index of vector array
    // map [ 2,4,5,6,9,11,12,13,15 ] into array of strings (quote ommitted)
    // --> [ "2;", "4-", "-", "6;", "9;", "11-", "-", "13;", "15;" ]
    vector = vector.map ( ( n, i, v ) => // n is current number at index i of vector v
        i < l && v [ i + 1 ] - n === 1 ? // next number is adjacent ? 
            `${ i > 0 && n - v [ i - 1 ] === 1 ? "" : n }-` :
            `${ n };`
        );
    return vector.join ( "" ).  // concatenate all strings in vector array
        replace ( /-+/g, "-" ). // replace multiple dashes by single dash
        slice ( 0, -1 );        // remove trailing ;
    }

If you want to add extra spaces for readability, just add extra calls to string.prototype.replace().

If the input vector is not sorted, you can add the following line right after the opening brace of the display() function:

vector.sort ( ( a, b ) => a - b ); // sort vector in place, in increasing order.

Note that this could be improved to avoid testing twice for integer adjacentness (adjacenthood? I'm not a native English speaker;-).

And of course, if you don't want a single string as output, split it with ";".

1

Rough outline of the process is as follows:

  • Create an empty array called ranges
  • For each value in sorted input array
    • If ranges is empty then insert the item {min: value, max: value}
    • Else if max of last item in ranges and the current value are consecutive then set max of last item in ranges = value
    • Else insert the item {min: value, max: value}
  • Format the ranges array as desired e.g. by combining min and max if same

The following code uses Array.reduce and simplifies the logic by combining step 2.1 and 2.3.

function arrayToRange(array) {
  return array
    .slice()
    .sort(function(a, b) {
      return a - b;
    })
    .reduce(function(ranges, value) {
      var lastIndex = ranges.length - 1;
      if (lastIndex === -1 || ranges[lastIndex].max !== value - 1) {
        ranges.push({ min: value, max: value });
      } else {
        ranges[lastIndex].max = value;
      }
      return ranges;
    }, [])
    .map(function(range) {
      return range.min !== range.max ? range.min + "-" + range.max : range.min.toString();
    });
}
console.log(arrayToRange([2, 3, 4, 5, 10, 18, 19, 20]));
Salman A
  • 262,204
  • 82
  • 430
  • 521
0

If you simply want a string that represents a range, then you'd find the mid-point of your sequence, and that becomes your middle value (10 in your example). You'd then grab the first item in the sequence, and the item that immediately preceded your mid-point, and build your first-sequence representation. You'd follow the same procedure to get your last item, and the item that immediately follows your mid-point, and build your last-sequence representation.

// Provide initial sequence
var sequence = [1,2,3,4,5,6,7,8,9,10];
// Find midpoint
var midpoint = Math.ceil(sequence.length/2);
// Build first sequence from midpoint
var firstSequence = sequence[0] + "-" + sequence[midpoint-2];
// Build second sequence from midpoint
var lastSequence  = sequence[midpoint] + "-" + sequence[sequence.length-1];
// Place all new in array
var newArray = [firstSequence,midpoint,lastSequence];

alert(newArray.join(",")); // 1-4,5,6-10

Demo Online: http://jsbin.com/uvahi/edit

Sampson
  • 265,109
  • 74
  • 539
  • 565
  • 6
    Wouldn't the output be 1-10, since the numbers 1-10 appear in sequence with none missing? – Nick Presta Feb 16 '10 at 07:53
  • I think you have misinterpreted the question logic @Sampson. The OP doesn't want to locate the mid-point then express the numbers on either side of the mid-point as a hyphenated expression. Your confusion isn't completely absurd, because the OP's data has a max integer of `20` and the only non-ranged value is `10` which just _happens_ to be the mid-point in terms of quality and position. The truth is, the OP wants to identify consecutive occurring values and express them as a hyphenated range. If a range only has one value, then no hyphen is necessary. Your data should output `1-10`. – mickmackusa May 29 '20 at 12:22
0
 ; For all cells of the array
    ;if current cell = prev cell + 1 -> range continues
    ;if current cell != prev cell + 1 -> range ended

int[] x  = [2,3,4,5,10,18,19,20]
string output = '['+x[0]
bool range = false; --current range
for (int i = 1; i > x[].length; i++) {
  if (x[i+1] = [x]+1) {
    range = true;
  } else { //not sequential
  if range = true
     output = output || '-' 
  else
     output = output || ','
  output.append(x[i]','||x[i+1])
  range = false;
  } 

}

Something like that.

glasnt
  • 2,865
  • 5
  • 35
  • 55
0

An adaptation of CMS's javascript solution for Cold Fusion

It does sort the list first so that 1,3,2,4,5,8,9,10 (or similar) properly converts to 1-5,8-10.

<cfscript>
    function getRanges(nArr) {
        arguments.nArr = listToArray(listSort(arguments.nArr,"numeric"));
        var ranges = [];
        var rstart = "";
        var rend = "";
        for (local.i = 1; i <= ArrayLen(arguments.nArr); i++) {
            rstart = arguments.nArr[i];
            rend = rstart;
            while (i < ArrayLen(arguments.nArr) and (val(arguments.nArr[i + 1]) - val(arguments.nArr[i])) == 1) {
                rend = val(arguments.nArr[i + 1]); // increment the index if the numbers sequential
                i++;
            }
            ArrayAppend(ranges,rstart == rend ? rstart : rstart & '-' & rend);
        }
        return arraytolist(ranges);
    }
</cfscript>
Community
  • 1
  • 1
Regular Jo
  • 5,190
  • 3
  • 25
  • 47
  • Thanks... I had written a similar UDF, but it was suffering from a bug. I updated this so it would accept a list or array and added a subroutine to trim & remove non-numeric items before attempting the numeric sort. (If this isn't done for user-provided values, an error may be thrown.) – James Moberg Aug 25 '16 at 15:16
0

Tiny ES6 module for you guys. It accepts a function to determine when we must break the sequence (breakDetectorFunc param - default is the simple thing for integer sequence input). NOTICE: since input is abstract - there's no auto-sorting before processing, so if your sequence isn't sorted - do it prior to calling this module

function defaultIntDetector(a, b){
    return Math.abs(b - a) > 1;
}

/**
 * @param {Array} valuesArray
 * @param {Boolean} [allArraysResult=false] if true - [1,2,3,7] will return [[1,3], [7,7]]. Otherwise [[1.3], 7]
 * @param {SequenceToIntervalsBreakDetector} [breakDetectorFunc] must return true if value1 and value2 can't be in one sequence (if we need a gap here)
 * @return {Array}
 */
const sequenceToIntervals = function (valuesArray, allArraysResult, breakDetectorFunc) {
    if (!breakDetectorFunc){
        breakDetectorFunc = defaultIntDetector;
    }
    if (typeof(allArraysResult) === 'undefined'){
        allArraysResult = false;
    }

    const intervals = [];
    let from = 0, to;
    if (valuesArray instanceof Array) {
        const cnt = valuesArray.length;
        for (let i = 0; i < cnt; i++) {
            to = i;
            if (i < cnt - 1) { // i is not last (to compare to next)
                if (breakDetectorFunc(valuesArray[i], valuesArray[i + 1])) {
                    // break
                    appendLastResult();
                }
            }
        }
        appendLastResult();
    } else {
        throw new Error("input is not an Array");
    }

    function appendLastResult(){
        if (isFinite(from) && isFinite(to)) {
            const vFrom = valuesArray[from];
            const vTo = valuesArray[to];

            if (from === to) {
                intervals.push(
                    allArraysResult
                        ? [vFrom, vTo] // same values array item
                        : vFrom // just a value, no array
                );
            } else if (Math.abs(from - to) === 1) { // sibling items
                if (allArraysResult) {
                    intervals.push([vFrom, vFrom]);
                    intervals.push([vTo, vTo]);
                } else {
                    intervals.push(vFrom, vTo);
                }
            } else {
                intervals.push([vFrom, vTo]); // true interval
            }
            from = to + 1;
        }
    }

    return intervals;
};

module.exports = sequenceToIntervals;

/** @callback SequenceToIntervalsBreakDetector
 @param value1
 @param value2
 @return bool
 */

first argument is the input sequence sorted array, second is a boolean flag controlling the output mode: if true - single item (outside the intervals) will be returned as arrays anyway: [1,7],[9,9],[10,10],[12,20], otherwise single items returned as they appear in the input array

for your sample input

[2,3,4,5,10,18,19,20]

it will return:

sequenceToIntervals([2,3,4,5,10,18,19,20], true) // [[2,5], [10,10], [18,20]]
sequenceToIntervals([2,3,4,5,10,18,19,20], false) // [[2,5], 10, [18,20]]
sequenceToIntervals([2,3,4,5,10,18,19,20]) // [[2,5], 10, [18,20]]
Roman86
  • 1,990
  • 23
  • 22
0

Here's a version in Coffeescript

getRanges = (array) -> 
    ranges = []
    rstart
    rend
    i = 0
    while  i < array.length
      rstart = array[i]
      rend = rstart
      while array[i + 1] - array[i] is 1
        rend = array[i + 1] # increment the index if the numbers sequential
        i = i  + 1
      if rstart == rend 
        ranges.push  rstart + ''
      else
        ranges.push rstart + '-' + rend
      i = i + 1
    return ranges
Sean Munson
  • 343
  • 2
  • 9
-1

I've written my own method that's dependent on Lo-Dash, but doesn't just give you back an array of ranges, rather, it just returns an array of range groups.

[1,2,3,4,6,8,10] becomes:

[[1,2,3,4],[6,8,10]]

http://jsfiddle.net/mberkom/ufVey/

Mike B
  • 2,660
  • 3
  • 20
  • 22