Simon
Simon

Reputation: 661

How to filter an array of numbers using different intervals?

In the example code below I want to filter numbersArray based on different intervals. An interval is defined by a combination of 2 arrays with the lower and upper bound of said interval. How do I identify matches or non-matches like below?

const numbersArray = [1,2,3,4,5,6,7,8,9,10];
const lowerBound = [1, 4, 8];
const higherBound = [3, 6, 10];

If the code works the following test should return true:

matches == [2, 5, 9];
nonmatches == [1, 3, 4, 6, 7, 8, 10];
  

Assume the arrays contains more than 1000 items and that the numbers doesn't follow any pattern.

Below is a less readable but more realistic scenario.

let numbersArray = [];
let lowerBound = [];
let higherBound = [];

for (let i = 0; i< 1000; i++){
  numbersArray.push(i);
}

for(let i = 0; i < 100; i++) {
  lowerBound.push(i * 10);
  higherBound.push((i * 10) + Math.random() * 10);
}

Upvotes: 12

Views: 1404

Answers (5)

3limin4t0r
3limin4t0r

Reputation: 21110

This approach is pretty straight forward. First zip lowerBound and higherBound together for convenience (this is optional). Then for each number in numbersArray, check if there is at least one match (using some) between a lower and higher bound. Add the number to the "match" or "non-match" array.

const numbersArray = [1,2,3,4,5,6,7,8,9,10];
const lowerBound = [1, 4, 8];
const higherBound = [3, 6, 10];

// zip the lower and higher bounds together
const bounds = lowerBound.map((_, i) => [lowerBound[i], higherBound[i]]);

const result = {true: [], false: []};
for (const number of numbersArray) {
  const match = bounds.some(([low, high]) => low < number && number < high);
  result[match].push(number);
}

console.log("matches:", ...result[true]);
console.log("non-matches:", ...result[false]);

Upvotes: 5

customcommander
customcommander

Reputation: 18901

I'm going to assume that we can change the data structure a little bit because this is quite awkward to work with:

const lowerBound = [1, 4, 8];
const higherBound = [3, 6, 10];

If the elements at the same indexes are meant to be together then let's just do that:

const bound = [[1, 3], [4, 6], [8, 10]];

Will come to bound later.

Now let's build a curried function that validates n if a < n && n < b:

const between = (a, b) => n => a < n && n < b;

const x = between(1, 3);
const y = between(4, 6);

x(1); // false
x(2); // true
y(1); // false
y(5); // true

Now let's build another curried function that validates n if at least one function returns true:

const or = (...fns) => n => fns.some(fn => fn(n));

const check = or(x, y);

check(1); // false
check(2); // true
check(5); // true

We will now transform bound into an or function after we transformed each pair into a between function:

const bound = [[1, 3], [4, 6], [8, 10]];
const check = or(...bound.map(([a, b]) => between(a, b)));

check is now a function that takes an n and returns true if n is between 1 and 3 or between 4 and 6, ...

const between = (a, b) => n => a < n && n < b;
const or = (...fns) => n => fns.some(fn => fn(n));

const bound = [[1, 3], [4, 6], [8, 10]];
const check = or(...bound.map(([a, b]) => between(a, b)));

const [nomatch, match] =
  [1,2,3,4,5,6,7,8,9,10].reduce(
    (acc, n) =>
      (acc[+check(n)].push(n), acc),
        [[], []]);

console.log(`match: [${match}]`);
console.log(`no match: [${nomatch}]`);

Upvotes: 11

Nulji
Nulji

Reputation: 457

Is that what you want ?

I'm using reduce to create an array of matches where I put a new element when this one matches (between lowerBound and higherBound) and for the nonmatches, just a filter of the numbers in numbersArray which are not presents in matches.

const numbersArray = [1,2,3,4,5,6,7,8,9,10];
const lowerBound = [1, 4, 8];
const higherBound = [3, 6, 10];

const matches = lowerBound.reduce((acc, val, i) => {
  return [
    ...acc,
    ...numbersArray.filter(o => !acc.includes(o) && o > lowerBound[i] && o < higherBound[i])
    ]
}, []);

const nonmatches = numbersArray.filter(o => !matches.includes(o));
console.log(matches);
console.log(nonmatches);

Upvotes: 2

lawrence-witt
lawrence-witt

Reputation: 9354

Tweaked my approach slightly to avoid the second filter loop. It's just a case of testing each number in the array against the implicit pair of lowerBound and higherBound. You can utilise the iteration variable in some to achieve this quite concisely, assuming the arrays have been properly formatted beforehand and there are no loose ends.

const numbersArray = [1,2,3,4,5,6,7,8,9,10];
const lowerBound = [1, 4, 8];
const higherBound = [3, 6, 10];

let matches = [];
let nonMatches = [];
  
numbersArray.forEach(num => {
  const matched = lowerBound.some((bound, i) => {
    return num > bound && num < higherBound[i];
  });

  matched ? matches.push(num) : nonMatches.push(num);
});

console.log(matches, nonMatches);

Upvotes: 5

Icepickle
Icepickle

Reputation: 12796

I think I would extract the lowerbound and upperbound values first into a list of predicates, and then just iterate those per number in the array. Depending if one matches, it either goes into the match result or the non-match result.

function filtered( array, lower, upper) {
  const predicates = lower.map( (v, i) => (value) => value > v && value < upper[i] );
  return array.reduce( (agg, cur) => {
    if (predicates.some( predicate => predicate(cur) )) {
      agg[0].push(cur);
    } else {
      agg[1].push(cur);
    }
    return agg;
  }, [[],[]]);
}

function simpleTest() {
  const numbersArray = [1,2,3,4,5,6,7,8,9,10];
  const lowerBound = [1, 4, 8];
  const higherBound = [3, 6, 10];

  const [matches, nonmatches] = filtered( numbersArray, lowerBound, higherBound );

  console.log( 'matches' );
  console.log( matches );
  console.log( 'matches' );
  console.log( nonmatches );
}

function suggestedTest() {
  // with suggested test
  let numbersArray = [];
  let lowerBound = [];
  let higherBound = []
  for (let i = 0; i< 1000; i++){
    numbersArray.push(i);
  }
  for(let i=0;i<100;i++) {
    lowerBound.push(i*10);
    higherBound.push((i*10)+Math.random()*10);
  }

  const [matches, nonmatches] = filtered( numbersArray, lowerBound, higherBound );
  console.log( 'matches' );
  console.log( matches );
  console.log( 'matches' );
  console.log( nonmatches );
}

console.log('basic');
simpleTest();

console.log('suggested');
suggestedTest();

Personally, I would also check if the lowerbound & upperbound arrays have the same length, but the question doesn't seem to define a behavior for this scenario. I'm also not sure what should happen in case of overlapping ranges, but these are all not specified

Upvotes: 6

Related Questions