Skip to content

How to split an array into a number of arrays based on increasing value by a given number?

I’ve written the following function:

const trends = hits.reduce((arr, curr, index, array) => {
      if (arr.includes(curr)) return arr
      if (curr + 1 === array[index + 1]) arr.push(curr, array[index + 1]);
      return arr;
    }, []);

The point is so that if an array contains a sequence of numbers which increase by 1 then this returns a new array with these values. For instance: [1, 2, 3, 6, 10] would return [1, 2, 3].

The problem is: if there’s more than one sequence, I’d like to have it in a separate array (or in an array of subarrays). At this point, the function does the following [1, 2, 3, 6, 7, 8]. I also can’t predict how many trends there might be. How can I accomplish this?

Answer

A straightforward approach, based on two conditions whose precedence can not be changed/swapped, which actually also reads what it does …

function collectItemSequences(list, item, idx, arr) {
  if ((item - 1) === arr[idx - 1]) {
    // in case of a predecessor ...

    // ... push item into the most recent sequence list.
    list[list.length - 1].push(item);

  } else if ((item + 1) === arr[idx + 1]) {
    // else, in case of a successor ...

    // ... create a new sequence list with its 1st item.
    list.push([ item ]);
  }
  return list;
}

console.log(
  [2, 4, 6, 8, 10, 12, 14].reduce(collectItemSequences, [])
);
console.log(
  [2, 4, 5, 6, 8, 10, 11, 12, 14].reduce(collectItemSequences, [])
);
console.log(
  [1, 2, 4, 5, 6, 8, 10, 11, 12, 14, 15].reduce(collectItemSequences, [])
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Based on the above approach one could implement a more generic one which allows the configuration of how to compute a current item’s sequence predecessor respectively sequence successor

function collectItemSequencesByConditions(collector, item, idx, arr) {
  const { getPredecessor, getSuccessor, list } = collector;
  if (getPredecessor(item) === arr[idx - 1]) {

    // push item into the most recent sequence list.
    list[list.length - 1].push(item);

  } else if (getSuccessor(item) === arr[idx + 1]) {

    // create a new sequence list with its 1st item.
    list.push([ item ]);
  }
  return collector;
}

const conditions = {
  getPredecessor: currentItem => currentItem - 2,
  getSuccessor: currentItem => currentItem + 2,
};

console.log(
  [2, 4, 6, 8, 10, 12, 14].reduce(
    collectItemSequencesByConditions,
    { ...conditions, list: [] },
  ).list
);
console.log(
  [2, 4, 5, 6, 8, 10, 11, 12, 14].reduce(
    collectItemSequencesByConditions,
    { ...conditions, list: [] },
  ).list
);
console.log(
  [1, 2, 4, 5, 6, 8, 10, 11, 12, 14, 15].reduce(
    collectItemSequencesByConditions,
    { ...conditions, list: [] },
  ).list
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Edit

The OP’s Q

I set up two pair of conditions, one for item - 1, item + 1, second pair respectively for - 10, + 10. The hits array was [22, 31, 32, 33, 42, 52]. I turned your console.logs into const variable = hits.reduce... so on. Then I returned both variables. The results were [31, 32, 33] and [42, 52]. The expected outcome for second is of course [22, 33, 42, 52].

Firstly the OP most probably meant [22, 32, 42, 52].

Secondly …

Nope, math is reliable. And the algorithm can’t be tricked. The rules which are applied for valid predecessors/successors are merciless. Thus the “expected outcome for” [22, 31, 32, 33, 42, 52] and +/- 10 of cause is [42, 52] and not [22, 32, 42, 52].

Why?.. The second value of [22, 31, 32, 33, 42, 52] is 31 which breaks any possible sequence (the OP expected 22, 32). Thus it is not a valid predecessor/successor sequence.

Here are some test cases …

console.log(
  "for [22, 31, 32, 33, 42, 52] and [-1 , +1]",
  "nexpect: '[[31,32,33]]' ?",
  JSON.stringify([22, 31, 32, 33, 42, 52].reduce(
    collectItemSequencesByConditions, {
      getPredecessor: currentItem => currentItem - 1,
      getSuccessor: currentItem => currentItem + 1,
      list: [],
    }
  ).list) === '[[31,32,33]]'
);
console.log(
  [22, 31, 32, 33, 42, 52].reduce(
    collectItemSequencesByConditions, {
      getPredecessor: currentItem => currentItem - 1,
      getSuccessor: currentItem => currentItem + 1,
      list: [],
    }
  ).list
);

console.log(
  "for [22, 31, 32, 33, 42, 52] and [-10 , +10]",
  "nexpect: '[[42,52]]' ?",
  JSON.stringify([22, 31, 32, 33, 42, 52].reduce(
    collectItemSequencesByConditions, {
      getPredecessor: currentItem => currentItem - 10,
      getSuccessor: currentItem => currentItem + 10,
      list: [],
    }
  ).list) === '[[42,52]]'
);
console.log(
  [22, 31, 32, 33, 42, 52].reduce(
    collectItemSequencesByConditions, {
      getPredecessor: currentItem => currentItem - 10,
      getSuccessor: currentItem => currentItem + 10,
      list: [],
    }
  ).list
);

console.log(
  "for [21, 22, 32, 33, 42, 52] and [-10 , +10]",
  "nexpect: '[[22,32],[42,52]]' ?",
  JSON.stringify([21, 22, 32, 33, 42, 52].reduce(
    collectItemSequencesByConditions, {
      getPredecessor: currentItem => currentItem - 10,
      getSuccessor: currentItem => currentItem + 10,
      list: [],
    }
  ).list) === '[[22,32],[42,52]]'
);
console.log(
  [21, 22, 32, 33, 42, 52].reduce(
    collectItemSequencesByConditions, {
      getPredecessor: currentItem => currentItem - 10,
      getSuccessor: currentItem => currentItem + 10,
      list: [],
    }
  ).list
);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
  function collectItemSequencesByConditions(collector, item, idx, arr) {
    const { getPredecessor, getSuccessor, list } = collector;
    if (getPredecessor(item) === arr[idx - 1]) {

      // push item into the most recent sequence list.
      list[list.length - 1].push(item);

    } else if (getSuccessor(item) === arr[idx + 1]) {

      // create a new sequence list with its 1st item.
      list.push([ item ]);
    }
    return collector;
  }
</script>