2

I have an array of objects(with keys: name, quoteNumber)and I would like to find the closest quoteNumber that is smaller than a given number then retrieve that object's name, I have consider using a for loop to remove the larger values, and get the max value from the remaining ones ,yet it might not be the best option given how large the dataset would be. Is this any other algorithm that is more efficient? Thanks!

const givenNum = 45
var array = [ 

    { name: "Sally",
      quoteNumber: 35},
    { name: "Jane",
      quoteNumber: 20},
    { name: "Edwin",
      quoteNumber: 55},
    { name: "Carrie",
      quoteNumber: 47}];

//'result:' Sally

efgdh
  • 315
  • 5
  • 14

4 Answers4

6

If it's unsorted, about the most efficient you can be is a single pass.

function getHighestQuote(quotes, limit) {
  let winner = null;
  let winningQuote = null;
  for (let {name, quoteNumber} of quotes) {
    if (quoteNumber > limit)
      continue;
    if (winningQuote === null || winningQuote < quoteNumber) {
      winner = name;
      winningQuote = quoteNumber;
    }
  }
  return winner;
}

It's not quite as snazzy as a functional approach, but it's a single linear-time pass that only needs to allocate a few stack variables.

Jeremy Roman
  • 16,137
  • 1
  • 43
  • 44
  • By the way, if you expect performance to matter, it's not a bad idea to benchmark with a representative data set. Here's a quick example (I don't know what your data actually looks like): https://jsbench.me/ebkm29r1xu/1 – Jeremy Roman Mar 09 '21 at 17:11
  • "only needs to allocate a few stack variables" could be optimized by using `for (var {name, quoteNumber} ...` instead of `let` since it will reuse the variable reference instead of creating a new one. `var` isn't completely dead ;) – mhodges Mar 09 '21 at 17:14
  • I believe modern JS engines will eliminate that difference (and I'd worry more about the `for..of` needing to generate code to deal with non-array deopts), but I haven't checked the assembly. Feel free to benchmark in your browser of choice if the difference may matter. :) – Jeremy Roman Mar 09 '21 at 17:16
  • Nothing wrong with being semantically correct if you're not sure what the JS engine will do - especially if performance is a concern. Or do people really hate `var` that much? – mhodges Mar 09 '21 at 17:19
  • 1
    I'm upvoting this answer. Although I like the succinctness of mine this is 100% the fastest way with a large dataset – Jamiec Mar 09 '21 at 17:22
  • 1
    Having played with the snazzy benchmark thing a bit. Using `for(let i = 0; i < quotes.length; i++)` etc. seems a fair bit faster, unless I've done something wrong. – Ben Stephens Mar 09 '21 at 18:17
  • Yes, that makes some sense -- for...of iteration needs to deal with non-arrays and can be somewhat slower. It's hard to beat indexed iteration on plain arrays. – Jeremy Roman Mar 09 '21 at 19:06
4

If you have a large dataset, you want to avoid doing anything which either loops over the array more than once, or even attempts to sort it before looking. A simple aggregate operation will do this fine. Use reduce

const givenNum = 45
var array = [ 

    { name: "Sally",
      quoteNumber: 35},
    { name: "Jane",
      quoteNumber: 20},
    { name: "Edwin",
      quoteNumber: 55},
    { name: "Carrie",
      quoteNumber: 47}];
      

const result = array.reduce ( (acc,item) => {
    const diff = givenNum - item.quoteNumber;
    if(item.quoteNumber < givenNum && diff < acc.diff)
       acc = {diff,  item}
    return acc;
},{ diff: Number.MAX_SAFE_INTEGER, item: null });

console.log(result.item);

Note also that if speed is really important avoid this solution too - it has extra method calls you won't have with with the simpler loop solution. That will always be the fastest option.

Jamiec
  • 133,658
  • 13
  • 134
  • 193
  • Perfect use case for `.reduce()`. This would be my preferred solution – mhodges Mar 09 '21 at 17:12
  • That reduce statement can be shorten to: `const result = array.reduce((pv, cv) => cv.quoteNumber <= givenNum && cv.quoteNumber > (pv?.quoteNumber ?? 0) ? cv : pv, null)` – andi2.2 Mar 09 '21 at 17:26
  • 1
    @andi2.2 It can indeed, but somewhat bizarrely on my browser that is slower than mine (https://jsbench.me/lekm2aluqw/1) whereas logically (to me at least) it looks like it should be quicker – Jamiec Mar 09 '21 at 17:34
  • @Jamiec Thanks for your comment on my (now deleted) post. Looking at the benchmark thingy, if I set `quoteNumber: Math.random() * 100` to `quoteNumber: Math.random() * 10000` I get Foo_reduceFind2 as the quickest response with foo_filterSort at just 14.66% slower. If you set it to Math.random() * 100000 (to match the number of entries) Foo_reduceFind, Foo_reduceFind2 and foo_filterSort come out as joint fastest. – Ben Stephens Mar 09 '21 at 17:48
  • @Jamiec That was in Firefox. In Chrome Foo_reduceFind, Foo_reduceFind2 and foo_filterSort are all tied but foo_forLoop is much faster. – Ben Stephens Mar 09 '21 at 17:58
  • @Jamiec Apologies for spamming a bit, setting the test case to have 50000 as the limit and foo_filterSort is bad again. – Ben Stephens Mar 09 '21 at 18:24
  • Thank you! I applied this algorithm and it works perfectly! – efgdh Mar 10 '21 at 16:33
  • @yhshj Take note of my final paragraph! – Jamiec Mar 10 '21 at 16:51
0

I've made a simple one-liner that kicks out all elements with a larger quote-number, sorts the other elements and finally get's the first element(-> the closest one):

const closest_smaller = array.filter(a => a.quoteNumber <= givenNum).sort((a,b) => b.quoteNumber-a.quoteNumber)[0]

If the givenNum is smaller than every quoteNumber, this returns undefined. if you want to have a fallback use:

const closest_smaller = array.filter(a => a.quoteNumber <= givenNum).sort((a,b) => b.quoteNumber-a.quoteNumber)[0] ?? 0

You also can use find instead of filter, i don't exactly know which one is faster:

const closest_smaller = array.sort((a,b) => b.quoteNumber-a.quoteNumber).find(a => a.quoteNumber <= givenNum) ?? 0
andi2.2
  • 86
  • 1
  • 11
  • Question had the magic words "Large dataset" so you want to avoid amnything `O(nlogn)` like sort. – Jamiec Mar 09 '21 at 17:04
0

This is based on @Jeremy Roman's and @Jamiec's answers. I think this is a good bit faster unless I've done something stupid: https://jsbench.me/g0kmha8buo/1

const array = [
  { name: "Sally",
    quoteNumber: 35},
  { name: "Jane",
    quoteNumber: 20},
  { name: "Velma",
    quoteNumber: 31},
  { name: "Edwin",
    quoteNumber: 55},
  { name: "Neva",
    quoteNumber: 30},
  { name: "Carrie",
    quoteNumber: 47},
  { name: "Arnold",
    quoteNumber: 29},
];

function closest_quote_less_than_or_equal_to(quotes, limit) {
  let winner = null;
  let winningQuote = 0;

  const limit_plus_one = limit + 1;

  for(let i = 0; i < quotes.length; i++) {
    const quoteNumber = quotes[i].quoteNumber;
    if(((quoteNumber < limit_plus_one) * quoteNumber) > winningQuote) {
      winningQuote = quoteNumber;
      winner = quotes[i].name;
    }
  }

  return winner;
}

console.log(closest_quote_less_than_or_equal_to(array, 45));
console.log(closest_quote_less_than_or_equal_to(array, 30));
console.log(closest_quote_less_than_or_equal_to(array, 10));
Ben Stephens
  • 3,303
  • 1
  • 4
  • 8