Given a number: find the closest value in a list of unique and sorted numbers with some twists I have a method getClosest() in an object, that does the following:


  Given a number: Find the closest number in a list of unique and sorted numeric values.


On top of that it has two additional ptions next|offset:


default: find the closest value, smaller or larger than the submitted value
next: find the next value in the list, that is larger than the submitted one
offset: like default but find the smaller value only, if the distance to it is smaller/equal than a given delta




You can also assume that:


the input value will always be smaller then or equal to the largest value in the set
all parameters have the correct type (integer|boolean)
the list will contain between 10-20 values




This is an example of the list:

this.values = [0, 35, 90, 110, 160, 175, 200];


The function returns an object, holding the position as well as the actual value:

{
    index: 1,
    value: 35
}






class Test {
  constructor() {
    this.values = [0, 35, 90, 110, 160, 175, 200];
    this.delta = 12;
  }

  getClosest(offset, next, value) {
    let closest,
        comparativeValue;

    if (offset === true) {
      for (let i = 0; i &lt; this.values.length; ++i) {
        const current = this.values[i];

        if (value &lt;= current) {
          closest = {
            index: i,
            value: current
          };
          break;
        }

        if (value &gt; current) {
          comparativeValue = {
            index: i,
            value: current
          };
        }
      }

      if (comparativeValue) {
        const offsetA = Math.abs(closest.value - value);
        const offsetB = Math.abs(comparativeValue.value - value);

        if (this.delta &lt; offsetA &amp;&amp; this.delta &gt; offsetB) {
          return comparativeValue;
        }
      }
    }

    if (next === true) {
      for (let i = 0; i &lt; this.values.length; ++i) {
        const current = this.values[i];

        if (current &gt; value) {
          closest = {
            index: i,
            value: current
          };
          break;
        }
      }
    }

    if (!offset &amp;&amp; !next) {
      for (let i = 0, shortest = null; i &lt; this.values.length; ++i) {
        const current = this.values[i];
        const delta = Math.abs(current - value);

        if (null === shortest || shortest &gt; delta) {
          closest = {
            index: i,
            value: current
          };
          shortest = delta;
        }
      }
    }
    return closest;
  }
}

const test = new Test();

console.log(
  test.getClosest(false, false, 10).value,
  test.getClosest(false, false, 100).value,

  test.getClosest(false, true, 10).value,
  test.getClosest(false, true, 100).value,

  test.getClosest(true, false, 10).value,
  test.getClosest(true, false, 100).value,
);






My concerns are:


overcomplicated
this ain't DRY at all
too many loops and variable declarations
there's a call getClosest(true, true, 40), which might lead to unexpected results




You can ignore the Test-object-wrapper. It's to simplify the example and to make the code easily runnable. Here's a fiddle as well to play around with.
The given specification is incomplete and your implementation differs from the specification.


For example, according to your specification, test.getClosest(true, false, 99).value should be 90 instead of 110, since the distance from 99 to the closest value 90 is 9 which is "smaller/equal than [the] given delta" of 12. 


Also, it is not clear how strong the given assumptions are and whether or not violations of those assumptions can occur and should be dealt with.


For example, your implementation fails on lists of less than 4 values. Now, one could say that this isn't a problem due to the assumption that a "list will contain between 10-20 values". But it is  generally considered bad practice and a more robust implementation would be recommended.


In addition to that, we don't know which parts of your interface or method signature are given and which parts are subject to review. You write that we "can ignore the Test-object-wrapper". But what about the method signature with its unorthodox choice of arguments, which seems to be required by the given specification?

For all these reasons, it is hard to provide a meaningful review.

Some notes:


getClosest(offset, next, value)


the get prefix usually indicates a lookup or simple computation. If more complex computations are required instead, I prefer using another verb such as findClosest.
Also, I recommend renaming offset to something more appropriate - it clearly isn't an offset.
Most developers would probably expect the value as the first argument, similar to built-in functions such as parseInt(string, radix) or Array.prototype.includes(value, offset).

if (null === shortest || shortest &gt; delta) { ... }


putting the literal value on the left side of the comparison operator helps to detect some typos early on, when one accidentally uses the assignment operator = instead of the comparison operator == or === within a comparison expression. However, this also makes the comparison more difficult to read. I prefer the other way around.

const offsetA = Math.abs(closest.value - value)


this is actually a delta and not an offset. I suggest const deltaNext or similar.

let closest, comparativeValue


closest is not necessarily the closest value. It is the next larger value.
Also, consistency would demand either attaching the redundant Value suffix to both identifiers or none.
Putting each variable declaration on its own line improves readability, but I also prefer giving each declaration its own let as this simplifies modifications in the future.

if (offset === true) { ... }


instead of strict comparison, I'd simply check for a truthy value via if (offset) { ... }. This duck typing approach fits better with the language. You already use it when writing if (!offset &amp;&amp; !next) { ... } or if (comparativeValue) { ... }.

shortest = delta


now you have closest (which is actually the next larger value), comparativeValue, shortest (which is actually the minimum delta), delta, this.delta, offset (which is actually a boolean flag) etc. It is hard to follow your naming scheme. I suggest more descriptive and more consistent naming.
Also, I wouldn't declare both an instance property this.delta and a local variable delta with different semantics.



Further suggestions:


It is not obvious to me why the arguments this.values and this.delta are given as instance properties and defined within the constructor function. By doing so, you create a stateful object which is more difficult to understand and test than e.g. a simple 'pure' function.
Also, by setting the next flag, the semantics of your method are no longer compatible to the meaning of getClosest. How about two methods findClosestEntry and findCeilingEntry? The Entry suffix indicates that the return value is a complex object containing index and value, comparable to e.g. Array.prototype.entries or Object.entries. Those established methods iterate or return [index, value] arrays instead of {index, value} objects, so you might want to consider following this established pattern, too.
Instead of passing an offset flag and relying on instance properties this.delta and this.values I suggest a method signature such as findClosestEntry(array, element, delta) and passing (or not passing) a delta argument.
I would replace the many for-loops with a single const nextIndex = values.findIndex(element =&gt; element &gt; value). You would then have to lookup const prevValue = values[nextIndex - 1] and const nextValue = values[nextIndex] and return either one depending on the given flags. Don't forget to test for the corner case when nextIndex === -1 and consider handling it appropriately.
If you had to handle more than 20 values, I'd implement a binary search and - if the number of queries for a given array of values is large, built a binary search tree first. This would improve runtime complexity from linear to logarithmic. But for just up to 20 values, the built-in Array.prototype.findIndex or even an explicit for-loop is surely faster.