3

I'm making an "acceleration" array like this:

acc["0100"] = 1;
acc["0300"] = 2;
acc["0600"] = 4;
acc["0900"] = 8;
acc["2000"] = 16;
acc["5000"] = 32;

And, when the user presses a key, I start a timer: this._startTick = (new Date()).getTime();

Now I have a timer that checks if the key is still pressed. If so, then I do something like:

this._delay = (new Date()).getTime() - this._startTick;

And now, based on this._delay, I'd like to find one of the previous values (1, 2, 4 or 8). How would you do that?

NB: if the value is greater than "5.0" then the result should always be 32.

NOTA: my goal is, given an elapsed time, find out which value is the best. I started the way I've just explained, but if you have another solution, I'll take it!

Olivier Pons
  • 15,363
  • 26
  • 117
  • 213

6 Answers6

2

Here is the jsfiddle test page.

var getAccForDelay = (function () {
    var acc = {
        0.1: 1,
        0.3: 2,
        0.6: 4,
        0.9: 8,
        2.0: 16,
        5.0: 32
    };

    return function(delay) {
        var key,
            bestKey = undefined,
            absDiff, 
            absDiffMin = Number.MAX_VALUE;

        for (key in acc) {
            if (acc.hasOwnProperty(key)) {
                absDiff = Math.abs(delay - key);
                if (absDiffMin > absDiff) {
                    absDiffMin = absDiff;
                    bestKey = key;
                }
            }
        } 
        return bestKey === undefined ? undefined : acc[bestKey];
    };
}());

Test:

console.clear();
console.log(getAccForDelay(0));
console.log(getAccForDelay(0.33));
console.log(getAccForDelay(3.14));
console.log(getAccForDelay(123456.789));

Output:

1
2
16
32

=== UPDATE ===

The above solution doesn't utilize of the fact that acc is sorted by key. I optimized the code by replacing linear search with binary search, which is much faster on long arrays. Here is the test page.

var getAccForDelay = (function () {
    var accKey   = [ 0.1, 0.3, 0.6, 0.9, 2.0, 5.0 ],
        accValue = [   1,   2,   4,   8,  16,  32 ],
        accLength = accKey.length;

    return function(delay) {
        var iLeft, iMiddle, iRight;

        iLeft = 0;
        if (delay <= accKey[iLeft])
            return accValue[iLeft];
        iRight = accLength - 1;
        if (accKey[iRight] <= delay)
            return accValue[iRight];        
        while (true) {
            if (iRight - iLeft === 1)
                return delay - accKey[iLeft] < accKey[iRight] - delay ? accValue[iLeft] : accValue[iRight];
            iMiddle = ~~((iLeft + iRight) / 2);
            if (delay < accKey[iMiddle])
                iRight = iMiddle;
            else if (accKey[iMiddle] < delay)
                iLeft = iMiddle;
            else
                return accValue[iMiddle];
        }
    };
}());
kol
  • 27,881
  • 12
  • 83
  • 120
  • If `delay == 12345.456`, the value should be 8. How would you do? – Olivier Pons Aug 28 '13 at 15:01
  • @OlivierPons Your updated acc object made the problem harder, so I had to completely rewrite my answer. – kol Aug 28 '13 at 15:33
  • I don't understant the part "`if (acc.hasOwnProperty(key))`" because it's in a `for()` and `acc` will always have the property `key`... unless I'm missing something? – Olivier Pons Aug 28 '13 at 15:45
  • @OlivierPons I iterate over the properties of acc, and this hasOwnProperty check ensures that I won't accidentally use an inherited property. This is not needed in this particular case, but this is the "standard way" to iterate over an objects own properties, so I wrote the code this way. – kol Aug 28 '13 at 15:55
  • This is a very nice answer, I think I'll check it as good, even though, I'm hitting another big problem: slowing down. I think I'll implement something like here: http://gamedev.stackexchange.com/questions/20905/simple-speed-deceleration-with-variable-time-step or maybe from here: http://stackoverflow.com/questions/5100811/algorithm-to-control-acceleration-until-a-position-is-reached – Olivier Pons Aug 29 '13 at 14:30
  • @OlivierPons Have you verified that the above solution is slow? Or what do you think is behind the slowness? – kol Aug 29 '13 at 15:02
  • The `for (key in acc) {}` implies that for each re-calculation there a *full* loop on the *whole* array. Aadit M Shah's answer #1 code means there's a mean of 50% loop on the array. Not more. I mean... I'm looking for something that could return as soon as possible the answer. – Olivier Pons Aug 30 '13 at 19:44
  • @OlivierPons I've optimized the code, pls. check out my updated answer. – kol Aug 30 '13 at 21:42
  • Thank you very much for your idea. I've tried to add your code here http://jsperf.com/get-value-delay/2 . The optimization of `getValueFast()` seems to be the fastest one... – Olivier Pons Sep 03 '13 at 12:41
2

It's easier to operate on an array than on an object:

var accArr = [];
for (time in acc) {
    accArr.push({time: time, value: acc[time]});
}

Assuming you have an array, you can do:

function getValue(delay) {
    var diffs = accArr.map(function (e) { return Math.abs(e.time - delay); });
    return accArr[diffs.indexOf(Math.min.apply(null, diffs))].value;
}

EDIT:

Well, you didn't mention that this is a performance-critical function. In that case, I would recommend picking a granularity (e.g. 0.05, so the multiplier for delay is 20) and pre-calculating all values from 0 to MAX_DELAY:

var multiplier = 20,
    granularity = 1 / multiplier;

var delayValues = (function () {
    var result = [];
    for (var delay = 0; delay <= MAX_DELAY; delay += granularity) {
        result.push(getValue(delay));
    }
    return result;
})();

During the animation, fetching the value will be a simple lookup in a relatively small table:

function getValueFast(delay) {
    return (delayValues[Math.round(delay * multiplier)] || 
            delayValues[delayValues.length - 1])
}

JSPerf comparison between this solution and simple if statements shows they perform equally fast for searching around a middle value.

Olivier Pons
  • 15,363
  • 26
  • 117
  • 213
Dzinx
  • 55,586
  • 10
  • 60
  • 78
  • Easier or faster in term of execution time? – Olivier Pons Sep 01 '13 at 06:37
  • Easier as in "more convenient", because arrays have utility functions that objects don't: `map`, `indexOf`, etc. With 5 items in a list/object, don't bother with execution time optimization. – Dzinx Sep 02 '13 at 07:52
  • If I have 100 sprites and I have to recalculate acceleration each 1/60 s this means 60 * 100 * 5 = 30000 comparisons / seconds, instead of "Aadit M Shah" #1 suggestion (which would be a mean of 15000 comparisons / seconds). I think this is a "basic" optimization that I should take care of. I don't know if I'm wrong... – Olivier Pons Sep 02 '13 at 08:44
  • And 100 sprites in a game is few (count numbers for scoring, bubbles, hints, explosions, not to talk about collision detection between each "real" sprite (there's no collision to do with the previous ones)). – Olivier Pons Sep 02 '13 at 08:46
  • OK, added a version optimized for speed and not for elegance :) – Dzinx Sep 02 '13 at 09:48
  • So your last code implies that (1) the timer will never exceed the `MAX_DELAY` value and (2) the 'tick' of the timer should be a multiple of `0.05`? – Olivier Pons Sep 03 '13 at 10:11
  • I've updated your code to reflect something closer to what you did: http://jsperf.com/get-value-delay/2 And it's faster **`;-)`** – Olivier Pons Sep 03 '13 at 11:50
  • The timer can exceed `MAX_DELAY`, it's just there to limit the creation of `delayValues`. If the actual delay is greater, then the second part of `||` from getValueFast is used and the value of the last element of the array is returned. – Dzinx Sep 04 '13 at 11:54
1

In my humble opinion I think the best solution to this problem is to write a function which picks the best acceleration based on the time using if statements as follows:

function getAcceleration(time) {
    if (time < 0.20) return 1;
    if (time < 0.45) return 2;
    if (time < 0.75) return 4;
    if (time < 1.45) return 8;
    if (time < 3.50) return 16;
    return 32;
}

However this is a static solution. If that's alright with you then I recommend you use this method. On the other hand if you need a dynamic solution then use this instead:

var getAcceleration = createAccelerationMap(0.1, 0.3, 0.6, 0.9, 2.0, 5.0);

function createAccelerationMap(previous) {
    var length = arguments.length, limits = [];

    for (var i = 1; i < length;) {
        var current = arguments[i++];
        limits.push((previous + current) / 2);
        previous = current;
    }

    return function (time) {
        var length = limits.length, acceleration = 1;

        for (var i = 0; i < length;) {
            if (time < limits[i++]) return acceleration;
            acceleration *= 2;
        }

        return acceleration;
    };
}

Either way you may then use getAcceleration as follows:

console.log(getAcceleration(0));          // 1
console.log(getAcceleration(0.33));       // 2
console.log(getAcceleration(0.64));       // 4
console.log(getAcceleration(1.42));       // 8
console.log(getAcceleration(3.14));       // 16
console.log(getAcceleration(123456.789)); // 32

See the demo: http://jsfiddle.net/QepT7/

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
0

If the 0.1 is the number of seconds, and you want to round to 1 decimal you can do something this:

// 0.42332 * 10 = 4.2332 
// Math.round( ) will be 4
// 4 / 10 = 0.4
acc[ (Math.round(this._delay * 10) / 10).toString() ]
Niels
  • 48,601
  • 4
  • 62
  • 81
0
var seconds = this._delay.toString().substring(0,2)

console.log(acc[seconds]);

This is a straight-forward approach of your problem: First I convert the float to a string, second I cut off everything after the third character.

naloxx
  • 231
  • 1
  • 7
0

What units are you using?

this._startTick = (new Date()).getTime();
//       ms     =                 ms

this._delay = (new Date()).getTime() - this._startTick;
//     ms   =                 ms     -       ms

So to get to "0.1"/etc from milliseconds I'm assuming you are doing

(Math.floor(ms / 100) / 10).toString();

Why not just keep everything in ms/100 so you can use integers?

var acc = [];
acc[ 1] =  1;
acc[ 3] =  2;
acc[ 6] =  4;
acc[ 9] =  8;
acc[20] = 16;
acc[50] = 32;

Then you can create a "nearest" lookup function like this

function find(x) {
    var i = 0;
    x = x | 0; // The | 0 will cause a cast to int
    if (x < 0) x = 0;
    if (acc[x] !== undefined) return acc[x];
    if (x > acc.length) return acc[acc.length - 1];
    while (++i < acc.length) {
        if (acc[x - i] !== undefined) return acc[x - i];
        if (acc[x + i] !== undefined) return acc[x + i];
    }
}
find(this._delay / 100);

Now examples are

find(30);    // 16
find(100.5); // 32
find(0);     //  1
Paul S.
  • 64,864
  • 9
  • 122
  • 138
  • You're perfeclty right, thank you for your suggestion, I'll modify my code with this idea, but unfortunately this does not answer to my question (my keys problem). – Olivier Pons Aug 28 '13 at 15:04
  • And there's still one problem: if `(this._delay / 100) | 0` give an index out of range, I'd like it to give the highest possible value (`acc[4]`). How would you do? – Olivier Pons Aug 28 '13 at 15:06
  • @OlivierPons also, if you're expecting huge swathes of _undefined_ vs few defined, you might consider a lookup function closer to those described here http://stackoverflow.com/questions/8584902/get-nearest-number-out-of-array – Paul S. Aug 28 '13 at 15:22
  • You can also fill up all elements of `acc` with the appropriate values (e.g. `acc[2] = 1` and get rid of the awkward checks in the while loop. – Michael Foukarakis Aug 28 '13 at 15:38