Javascript's equivalent of R's findInterval() or Python's bisect.bisect_left

Question:

I can’t find how to determine to which interval an element belongs based on an Array for JavaScript. I want the behavior of bisect.bisect_left from Python. Here is some sample code:

import bisect
a = [10,20,30,40]
print(bisect.bisect_left(a,0))  #0  because 0 <= 10
print(bisect.bisect_left(a,10)) #0  because 10 <= 10
print(bisect.bisect_left(a,15)) #1  because 10 < 15 < 20
print(bisect.bisect_left(a,25)) #2  ...
print(bisect.bisect_left(a,35)) #3  ...
print(bisect.bisect_left(a,45)) #4

I know this would be easy to implement, but why re-invent the wheel?

Asked By: AFP_555

||

Answers:

There are no built-in bisection functions in JavaScript, so you will have to roll your own. Here is my personal reinvention of the wheel:

var array = [10, 20, 30, 40]

function bisectLeft (array, x) {
  for (var i = 0; i < array.length; i++) {
    if (array[i] >= x) return i
  }
  return array.length
}

console.log(bisectLeft(array, 5))
console.log(bisectLeft(array, 15))
console.log(bisectLeft(array, 25))
console.log(bisectLeft(array, 35))
console.log(bisectLeft(array, 45))

function bisectRight (array, x) {
  for (var i = 0; i < array.length; i++) {
    if (array[i] > x) return i
  }
  return array.length
}

Answered By: gyre

A faster way than the previously accepted answer that works for same size intervals is:

var array = [5, 20, 35, 50]

//Intervals:
//      <5: 0 
//  [5-20): 1
// [20-35): 2
// [35-50): 3
//    >=50: 4

var getPosition = function(array, x) {
  if (array.length == 0) return;
  if (array.length == 1) return (x < array[0]) ? 0 : 1;
  return Math.floor((x - array[0]) / (array[1] - array[0])) + 1
}

console.log(getPosition(array, 2)); //0
console.log(getPosition(array, 5)); //1
console.log(getPosition(array, 15));//1
console.log(getPosition(array, 20));//2
console.log(getPosition(array, 48));//3
console.log(getPosition(array, 50));//4
console.log(getPosition(array, 53));//4

console.log("WHEN SIZE: 1")
array = [5];
//Intervals:
//  <5: 0
// >=5: 1
console.log(getPosition(array, 3));
console.log(getPosition(array, 5));
console.log(getPosition(array, 6));

Answered By: AFP_555

In case anyone else lands here, here’s an implementation of bisect_left that actually runs in O(log N), and should work regardless of the interval between list elements. NB that is does not sort the input list, and, as-is, will likely blow the stack if you pass it an unsorted list. It’s also only set up to work with numbers, but it should be easy enough to adapt it to accept a comparison function. Take this as a starting point, not necessarily your destination. Improvements are certainly welcome!

Run it in a REPL

function bisect(sortedList, el){
  if(!sortedList.length) return 0;

  if(sortedList.length == 1) {
    return el > sortedList[0] ? 1 : 0;
  }

  let lbound = 0;
  let rbound = sortedList.length - 1;
  return bisect(lbound, rbound);

// note that this function depends on closure over lbound and rbound
// to work correctly
  function bisect(lb, rb){
    if(rb - lb == 1){
      if(sortedList[lb] < el && sortedList[rb] >= el){
        return lb + 1;
      }

      if(sortedList[lb] == el){
        return lb;
      }
    }

    if(sortedList[lb] > el){
      return 0;
    }

    if(sortedList[rb] < el){
      return sortedList.length
    }

    let midPoint = lb + (Math.floor((rb - lb) / 2));
    let midValue = sortedList[midPoint];

    if(el <= midValue){
      rbound = midPoint
    }

    else if(el > midValue){
      lbound = midPoint
    }

    return bisect(lbound, rbound);
  }
}

console.log(bisect([1,2,4,5,6], 3)) // => 2
console.log(bisect([1,2,4,5,6], 7)) // => 5
console.log(bisect([0,1,1,1,1,2], 1)) // => 1
console.log(bisect([0,1], 0)) // => 0
console.log(bisect([1,1,1,1,1], 1)) // => 0
console.log(bisect([1], 2)); // => 1
console.log(bisect([1], 1)); // => 0
Answered By: Ross Hunter

using the D3-array npm.

const d3 = require('d3-array'); 

var a = [10,20,30,40];
console.log(d3.bisectLeft(a,0));  
console.log(d3.bisectLeft(a,10)); 
console.log(d3.bisectLeft(a,15));
console.log(d3.bisectLeft(a,25));
console.log(d3.bisectLeft(a,35));
console.log(d3.bisectLeft(a,45));

output:

0
0
1
2
3
4
Answered By: Simon Cooper

Speaking of re-inventing the wheel, I’d like to join the conversation:

function bisectLeft(arr, value, lo=0, hi=arr.length) {
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (arr[mid] < value) {
      lo = mid + 1;
    } else {
      hi = mid;
    }
  }
  return lo;
}

I believe that is the schoolbook implementation of bisection. Actually, you’ll find something pretty much the same inside the d3-array package mentioned before.

Answered By: emu
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.