如何将数组中连续的整数简写为连字符范围表达式?

37
在JavaScript中,如何将数组中的一系列数字转换为数字范围?换句话说,我想将连续出现的整数(没有间隔)表示为带连字符的范围。
例如:[2,3,4,5,10,18,19,20] 可以变成 [2-5,10,18-20][1,6,7,9,10,12] 可以变成 [1,6-7,9-10,12][3,5,99] 仍然是 [3,5,99][5,6,7,8,9,10,11] 可以变成 [5-11]

你是如何确定范围的起始和结束位置的? - Sampson
2
我刚刚为此制作了一个npm包。sequence-to-range https://www.npmjs.com/package/sequence-to-range - Ram Prasad Agarwal
13个回答

39

这里是我一段时间前创作的算法,最初是用C#编写的,现在我将其转移到了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"]

9
建议先对数值进行排序,这样就可以处理混合值,例如:[1,3,2,6,5,7]。 - Tracker1

5

我只是在玩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;
  }

++ 用于技巧 while 循环。顺便说一下,这相当于 function getRanges(c){for(var b=[],a,d=0;d<c.length;)b.push((a=c[d])+(function(b){for(;++a===c[++d];);return--a===b}(a)?"":"-"+a));return b};(Google 闭包编译器)。 - Orwellophile

3
非常好的问题:这是我的尝试:
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


这个答案缺少解释。 - mickmackusa

3

今天我需要使用TypeScript代码来解决这个问题,这个问题是在很多年之后才出现的,我决定尝试一种比其他答案更加函数式的风格来编写代码。当然,只有参数和返回类型注释将此代码与标准的 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));
  }

请注意,slice 是必须的,因为 sort 是就地排序,我们无法更改原始数组。

1
使用 ES6,一个解决方案是:
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 ;
    }

如果您想添加额外的空格以提高可读性,请只需添加额外的string.prototype.replace()调用。
如果输入向量未排序,则可以在display()函数的左花括号后面添加以下行: vector.sort ( ( a, b ) => a - b ); // sort vector in place, in increasing order
请注意,这可以改进以避免两次测试整数相邻性(相邻?我不是母语英语人士;-)。
当然,如果您不想要单个字符串作为输出,请使用“;”进行拆分。

1

这是我的看法...

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;
}

1
大致过程如下:
  • 创建一个名为ranges的空数组
  • 对于排序后的输入数组中的每个value
    • 如果ranges为空,则插入{min: value, max: value}
    • 否则,如果ranges中上一个项的max和当前value连续,则将ranges中上一个项的max设置为value
    • 否则,插入{min: value, max: value}
  • 按需格式化ranges数组,例如通过组合minmax(如果相同)

以下代码使用Array.reduce并通过组合步骤2.1和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]));


0

CMS的JavaScript解决方案 改编为ColdFusion

它会对列表进行排序,使得1,3,2,4,5,8,9,10(或类似数字)正确地转换为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>

谢谢...我之前写了一个类似的UDF,但是它有一个bug。我更新了它,使它能够接受列表或数组,并添加了一个子程序来修剪和删除非数字项,然后再尝试进行数字排序。(如果不对用户提供的值进行此操作,则可能会引发错误。) - James Moberg

0

给大家提供一个小巧的ES6模块。它接受一个函数来确定我们何时必须中断序列(breakDetectorFunc参数-默认为整数序列输入的简单函数)。 注意:由于输入是抽象的,所以在处理之前没有自动排序,因此如果您的序列未排序,请在调用此模块之前进行排序。

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

第一个参数是输入序列排序数组,第二个参数是一个布尔标志,控制输出模式:如果为true-单个项(在间隔之外)将被返回为数组:[1,7],[9,9],[10,10],[12,20],否则单个项按照它们在输入数组中出现的顺序返回

对于您的示例输入

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

它将返回:

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]]

0
如果您只是想要表示范围的字符串,那么您需要找到序列的中点,并将其作为中间值(在您的示例中为10)。然后,您需要获取序列中的第一个项目和紧接在中点之前的项目,并构建您的第一个序列表示。您需要按照相同的过程获取最后一个项目和紧接在中点之后的项目,并构建您的最后一个序列表示。
// 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

在线演示:http://jsbin.com/uvahi/edit

6
输出结果会是1-10,因为数字1-10按顺序连续出现且没有遗漏。 - Nick Presta
我认为你误解了问题逻辑,@Sampson。OP不想找到中点,然后把中点两边的数字表示为连字符表达式。你的疑惑并不完全荒谬,因为OP的数据具有最大整数为“20”,而唯一的非范围值是“10”,这恰好是质量和位置方面的中点。事实上,OP想要识别连续出现的值,并将它们表示为一个带连字符的范围。如果一个范围只有一个值,则不需要连字符。你的数据应输出“1-10”。 - mickmackusa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接