esiale
esiale

Reputation: 83

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?

Upvotes: 0

Views: 334

Answers (2)

Peter Seliger
Peter Seliger

Reputation: 13376

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>

Upvotes: 1

trincot
trincot

Reputation: 350270

You could let the accumulator be a combination of the partial result array, and the previous value (for easier access to it). To keep the code simple, you could add every current value, either to the last subarray, or as its own subarray. After reduce finishes, you can kick out those subarrays that only have one value:

let hits = [2, 4, 5, 6, 8, 10, 11, 12, 14];

const trends = hits.reduce(([arr, prev], curr, index, array) => {
    if (prev + 1 == curr) arr[arr.length-1].push(curr);
    else arr.push([curr]);
    return [arr, curr];
}, [[], NaN])[0].filter(row => row.length > 1);
    
console.log(trends);

Upvotes: 1

Related Questions