So I've been scouring the web to find a way to do this but nothing I've found fits the exact solution we are looking for.
I have an app that stores float numbers in a circular buffer. The circular buffer class can be found here https://www.npmjs.com/package/circular-buffer.
In my app new numbers are coming in every few miliseconds and the buffer holds around 60K values.
For the purpose of this question though I created a circular buffer of 10 with a stream of 100 randomly generated numbers to simulate the incoming data. This is the line that generates the simulated stream:
for (let i = 0; i < valuesToEnqueueForDemoPurposes.length; i++) {
With my current setup, it is taking the cpu too much time to convert the circular buffer to an array and then calculate its min, max, min position / index number, max position / index number and standard deviations (but the stddev is not the focus of this question).
Here is my current code:
stats = require("stats-lite");
var CircularBuffer = require("circular-buffer");
var circularBuffer10 = new CircularBuffer(10);
var valuesToEnqueueForDemoPurposes = Array.from(Array(100)).map(x=>Math.random() * 1000)
for (let i = 0; i < valuesToEnqueueForDemoPurposes.length; i++) {
var newValue = valuesToEnqueueForDemoPurposes[i];
circularBuffer10.enq(newValue);
let valuesArray = circularBuffer10.toarray();
var maxIndex = valuesArray.reduce((iMax, x, i, arr) => x > arr[iMax] ? i : iMax, 0);
var minIndex = valuesArray.reduce((iMin, x, i, arr) => x < arr[iMin] ? i : iMin, 0);
var max = valuesArray[maxIndex];
var min = valuesArray[minIndex];
var standardDeviation = stats.stdev(valuesArray);
console.log(maxIndex);
console.log(max);
console.log(minIndex);
console.log(min);
console.log(standardDeviation + "\n\n");
}
So I was wondering if it was possible to optimize this code with different data structures.
The closest answer I've found to solve this issue is from this SO answer: https://stackoverflow.com/a/48610455
It uses:
a queue of N items
a Min / Max Heap to track the min / max item.
A hash map to track the frequency of each item.
But the problem with this solution is that the heap is always growing and with the amount of differing incoming data I receive, this would cause a serious problem. And it also only calculates the maximum.
Also found this c++ solution but it is only for a normal queue, a max (not min) and I wasn't able to reproduce in javascript:
https://www.geeksforgeeks.org/design-a-queue-data-structure-to-get-minimum-or-maximum-in-o1-time/
Does anyone know if it would be possible, using whatever combination of data structures, to find the Max or Min in O(1) for this type of scenario (with or without circular buffers)?
Thanks to #thomas's advice I was able to alter the circular buffer class I was using so that the sum, mean,max, min and standard deviations are calculated on average at O(1) but at the worst O(N). Worked like a charm for the performance I needed.
Please note that for my purposes I have only altered the "unshift" method of the CBuffer circular buffer class . So the other methods won't be updating the max, min and standard deviations correctly. Here is the link to the jsfiddle:
https://jsfiddle.net/29f4an7s/
And here is my testing code:
// A standard deviation object constructor. Running deviation (avoid growing arrays) which
// is round-off error resistant. Based on an algorithm found in a Knuth book.
class StandardDeviation {
constructor() {
this.v = 0;
this.w = 0;
this.S = 0;
this.count = 0;
}
// Add a measurement. Also calculates updates to stepwise parameters which are later used
// to determine sigma.
add(measurement) {
this.count += 1;
this.w = this.v;
this.v = this.v + (measurement - this.v) / this.count;
this.S = this.S + (measurement - this.w) * (measurement - this.v);
}
// Performs the final step needed to get the standard deviation and returns it.
get() {
if (this.count < 2) {
// There are less measurements accumulated than necessary to perform computation
return 0.0;
} else {
return Math.sqrt(this.S / (this.count));
}
}
// Replaces the value x currently present in this sample with the
// new value y. In a sliding window, x is the value that
// drops out and y is the new value entering the window. The sample
// count remains constant with this operation.
replace(x, y) {
const deltaYX = y - x;
const deltaX = x - this.v;
const deltaY = y - this.v;
this.v = this.v + deltaYX / this.count;
const deltaYp = y - this.v;
const countMinus1 = this.count - 1;
this.S = this.S - this.count / countMinus1 * (deltaX * deltaX - deltaY * deltaYp) - deltaYX * deltaYp / countMinus1;
}
// Remove a measurement. Also calculates updates to stepwise parameters which are later used
// to determine sigma.
remove(x) {
this.w = (this.count * this.v - x) / (this.count - 1);
this.S -= (x - this.v) * (x - this.w);
this.v = this.w;
this.count -= 1;
}
}
function CBuffer() {
// handle cases where "new" keyword wasn't used
if (!(this instanceof CBuffer)) {
// multiple conditions need to be checked to properly emulate Array
if (arguments.length > 1 || typeof arguments[0] !== 'number') {
return CBuffer.apply(new CBuffer(arguments.length), arguments);
} else {
return new CBuffer(arguments[0]);
}
}
// if no arguments, then nothing needs to be set
if (arguments.length === 0)
throw new Error('Missing Argument: You must pass a valid buffer size');
// this is the same in either scenario
this.length = this.start = 0;
// set to callback fn if data is about to be overwritten
this.overflow = null;
// set to callback fn if data is about to be overwritten
this.maxIndex = null;
this.minIndex = null;
this.max = null;
this.min = null;
this.sum = null;
this.mean = null;
this.standardDeviation = new StandardDeviation();
// emulate Array based on passed arguments
if (arguments.length > 1 || typeof arguments[0] !== 'number') {
this.data = new Array(arguments.length);
this.end = (this.size = arguments.length) - 1;
this.push.apply(this, arguments);
} else {
this.data = new Array(arguments[0]);
this.end = (this.size = arguments[0]) - 1;
}
// need to `return this` so `return CBuffer.apply` works
return this;
}
function defaultComparitor(a, b) {
return a == b ? 0 : a > b ? 1 : -1;
}
function mod(n, m) {
return ((n % m) + m) % m;
}
CBuffer.prototype = {
// properly set constructor
constructor : CBuffer,
/* mutator methods */
// pop last item
pop : function () {
var item;
if (this.length === 0) return;
item = this.data[this.end];
// remove the reference to the object so it can be garbage collected
delete this.data[this.end];
this.end = (this.end - 1 + this.size) % this.size;
this.length--;
return item;
},
// push item to the end
push : function () {
var i = 0;
var returnOverflow = false;
// check if overflow is set, and if data is about to be overwritten
if (this.overflow && this.length + arguments.length > this.size) {
// call overflow function and send data that's about to be overwritten
for (; i < this.length + arguments.length - this.size; i++) {
returnOverflow = this.data[(this.end + i + 1) % this.size];
// this.overflow(this.data[(this.end + i + 1) % this.size], this);
}
}
// push items to the end, wrapping and erasing existing items
// using arguments variable directly to reduce gc footprint
for (i = 0; i < arguments.length; i++) {
this.data[(this.end + i + 1) % this.size] = arguments[i];
}
// recalculate length
if (this.length < this.size) {
if (this.length + i > this.size) this.length = this.size;
else this.length += i;
}
// recalculate end
this.end = (this.end + i) % this.size;
// recalculate start
this.start = (this.size + this.end - this.length + 1) % this.size;
// return number current number of items in CBuffer
return returnOverflow;
},
// reverse order of the buffer
reverse : function () {
var i = 0,
tmp;
for (; i < ~~(this.length / 2); i++) {
tmp = this.data[(this.start + i) % this.size];
this.data[(this.start + i) % this.size] = this.data[(this.start + (this.length - i - 1)) % this.size];
this.data[(this.start + (this.length - i - 1)) % this.size] = tmp;
}
return this;
},
// rotate buffer to the left by cntr, or by 1
rotateLeft : function (cntr) {
if (typeof cntr === 'undefined') cntr = 1;
if (typeof cntr !== 'number') throw new Error("Argument must be a number");
while (--cntr >= 0) {
this.push(this.shift());
}
return this;
},
// rotate buffer to the right by cntr, or by 1
rotateRight : function (cntr) {
if (typeof cntr === 'undefined') cntr = 1;
if (typeof cntr !== 'number') throw new Error("Argument must be a number");
while (--cntr >= 0) {
this.unshift(this.pop());
}
return this;
},
// remove and return first item
shift : function () {
var item;
// check if there are any items in CBuff
if (this.length === 0) return;
// store first item for return
item = this.data[this.start];
// recalculate start of CBuffer
this.start = (this.start + 1) % this.size;
// decrement length
this.length--;
return item;
},
// sort items
sort : function (fn) {
this.data.sort(fn || defaultComparitor);
this.start = 0;
this.end = this.length - 1;
return this;
},
// add item to beginning of buffer
unshift : function () {
var i = 0;
var returnOverflow = false;
if (this.length == this.size) {
returnOverflow = this.last();
}
for (i = 0; i < arguments.length; i++) {
this.data[(this.size + this.start - (i % this.size) - 1) % this.size] = arguments[i];
}
if (this.size - this.length - i < 0) {
this.end += this.size - this.length - i;
if (this.end < 0) this.end = this.size + (this.end % this.size);
}
if (this.length < this.size) {
if (this.length + i > this.size) this.length = this.size;
else this.length += i;
}
this.start -= arguments.length;
if (this.start < 0) this.start = this.size + (this.start % this.size);
this.recalculateMaxMin(arguments[0], returnOverflow);
this.sum += arguments[0];
if (returnOverflow) {
this.sum -= returnOverflow;
this.standardDeviation.replace(returnOverflow, arguments[0])
}
else {
this.standardDeviation.add(arguments[0]);
}
this.mean = this.sum / this.length;
return returnOverflow;
},
/* accessor methods */
// return index of first matched element
indexOf : function (arg, idx) {
if (!idx) idx = 0;
for (; idx < this.length; idx++) {
if (this.data[(this.start + idx) % this.size] === arg) return idx;
}
return -1;
},
// return last index of the first match
lastIndexOf : function (arg, idx) {
if (!idx) idx = this.length - 1;
for (; idx >= 0; idx--) {
if (this.data[(this.start + idx) % this.size] === arg) return idx;
}
return -1;
},
// return the index an item would be inserted to if this
// is a sorted circular buffer
sortedIndex : function(value, comparitor, context) {
comparitor = comparitor || defaultComparitor;
var isFull = this.length === this.size,
low = this.start,
high = isFull ? this.length - 1 : this.length;
// Tricky part is finding if its before or after the pivot
// we can get this info by checking if the target is less than
// the last item. After that it's just a typical binary search.
if (low && comparitor.call(context, value, this.data[high]) > 0) {
low = 0, high = this.end;
}
while (low < high) {
var mid = (low + high) >>> 1;
if (comparitor.call(context, value, this.data[mid]) > 0) low = mid + 1;
else high = mid;
}
return !isFull ? low :
// http://stackoverflow.com/a/18618273/1517919
(((low - this.start) % this.length) + this.length) % this.length;
},
/* iteration methods */
// check every item in the array against a test
every : function (callback, context) {
var i = 0;
for (; i < this.length; i++) {
if (!callback.call(context, this.data[(this.start + i) % this.size], i, this))
return false;
}
return true;
},
// loop through each item in buffer
// TODO: figure out how to emulate Array use better
forEach : function (callback, context) {
var i = 0;
for (; i < this.length; i++) {
callback.call(context, this.data[(this.start + i) % this.size], i, this);
}
},
// construct new CBuffer of same length, apply map function, and return new CBuffer
map : function (callback, context) {
var outCBuffer = new CBuffer(this.size);
for (var i = 0; i < this.length; i++) {
var n = (this.start + i) % this.size;
outCBuffer.push(callback.call(context, this.data[n], i, this));
}
return outCBuffer;
},
// check items agains test until one returns true
// TODO: figure out how to emulate Array use better
some : function (callback, context) {
var i = 0;
for (; i < this.length; i++) {
if (callback.call(context, this.data[(this.start + i) % this.size], i, this))
return true;
}
return false;
},
// calculate the average value of a circular buffer
avg : function () {
return this.length == 0 ? 0 : (this.sum() / this.length);
},
// loop through each item in buffer and calculate sum
sum : function () {
var index = this.length;
var s = 0;
while (index--) s += this.data[index];
return s;
},
// loop through each item in buffer and calculate sum
getMaxPosition : function () {
// return 0
return (this.start + this.start + this.maxIndex) % this.size;
},
// loop through each item in buffer and calculate sum
getStandardDeviation : function () {
// return 0
return this.standardDeviation.get();
},
// loop through each item in buffer and calculate sum
getMinPosition : function () {
// return 0
return (this.start + this.start + this.minIndex) % this.size;
},
recalculateMaxMin : function (newValue, returnOverflow) {
if (this.length == 1) {
this.max = newValue;
this.maxIndex = this.start;
this.min = newValue;
this.minIndex = this.start;
return;
}
// Max / Mins
if (newValue > this.max) {
this.max = newValue;
this.maxIndex = this.start;
}
if (newValue < this.min) {
this.min = newValue;
this.minIndex = this.start;
}
// If overflow max or min recalculate
if (
returnOverflow && (returnOverflow >= this.max || returnOverflow <= this.min)
) {
this.maxIndex = 0;
this.minIndex = 0;
this.max = this.data[0];
this.min = this.data[0];
for (let i = 0; i < this.length; i++) {
if (this.data[i] > this.max) {
this.maxIndex = i;
this.max = this.data[i];
}
if (this.data[i] < this.min) {
this.minIndex = i;
this.min = this.data[i];
}
}
}
},
// loop through each item in buffer and calculate median
median : function () {
if (this.length === 0)
return 0;
var values = this.slice().sort(defaultComparitor);
var half = Math.floor(values.length / 2);
if(values.length % 2)
return values[half];
else
return (values[half-1] + values[half]) / 2.0;
},
/* utility methods */
// reset pointers to buffer with zero items
// note: this will not remove values in cbuffer, so if for security values
// need to be overwritten, run `.fill(null).empty()`
empty : function () {
var i = 0;
this.length = this.start = 0;
this.end = this.size - 1;
return this;
},
// fill all places with passed value or function
fill : function (arg) {
var i = 0;
if (typeof arg === 'function') {
while(this.data[i] = arg(), ++i < this.size);
} else {
while(this.data[i] = arg, ++i < this.size);
}
// reposition start/end
this.start = 0;
this.end = this.size - 1;
this.length = this.size;
return this;
},
// return first item in buffer
first : function () {
return this.data[this.start];
},
// return last item in buffer
last : function () {
return this.data[this.end];
},
// return specific index in buffer
get : function (arg) {
return this.data[mod(this.start + arg, this.size)];
},
isFull : function (arg) {
return this.size === this.length;
},
// set value at specified index
set : function (idx, arg) {
return this.data[(this.start + idx) % this.size] = arg;
},
// return clean array of values
toArray : function () {
return this.slice();
},
// return a string based on the array
join : function(separator) {
if (!separator) separator = ',';
var outString = new String(this.data[0]);
for (var i = 1; i < this.length; i++) {
var n = (this.start + i) % this.size;
outString = outString.concat(separator, this.data[i]);
}
return outString;
},
// slice the buffer to an arraay
slice : function (start, end) {
var size = this.length;
start = +start || 0;
if (start < 0) {
if (start >= end)
return [];
start = (-start > size) ? 0 : size + start;
}
if (end == null || end > size)
end = size;
else if (end < 0)
end += size;
else
end = +end || 0;
size = start < end ? end - start : 0;
var result = Array(size);
for (var index = 0; index < size; index++) {
result[index] = this.data[(this.start + start + index) % this.size];
}
return result;
}
};
var bufferLength = 3;
var numbersToGenerate = 10;
var circularBufferN = new CBuffer(bufferLength);
var valuesToEnqueueForDemoPurposes = Array.from(Array(numbersToGenerate)).map(x=>Math.random() * 1000)
for (let i = 0; i < valuesToEnqueueForDemoPurposes.length; i++) {
var newValue = valuesToEnqueueForDemoPurposes[i];
console.log("\n\nNEW VALUE****************************************************************:");
console.log(newValue);
console.log("STARTING UNSHIFT:");
console.log(circularBufferN.unshift(newValue));
let valuesArray = circularBufferN.data;
var maxIndex = circularBufferN.maxIndex;
var minIndex = circularBufferN.minIndex;
var max = valuesArray[maxIndex];
var min = valuesArray[minIndex];
console.log("Max Index");
console.log(maxIndex);
console.log("Max:");
console.log(max);
console.log("Min Index:");
console.log(minIndex);
console.log("Min:");
console.log(min);
console.log("Start:");
console.log(circularBufferN.start);
console.log("ORDERED ARRAY:");
console.log(circularBufferN.toArray());
console.log("Max Position:");
console.log(circularBufferN.getMaxPosition());
console.log("Min Position:");
console.log(circularBufferN.getMinPosition());
console.log('Sum:');
console.log(circularBufferN.sum);
console.log("mean:");
console.log(circularBufferN.mean);
console.log("Derived Standard Deviation");
console.log(circularBufferN.getStandardDeviation());
}
Related
So I have this class which is used for shape morphing:
class ShapeOverlays {
constructor(elm) {
this.elm = elm;
this.path = elm.querySelectorAll('path');
this.numPoints = 18;
this.duration = 600;
this.delayPointsArray = [];
this.delayPointsMax = 300;
this.delayPerPath = 100;
this.timeStart = Date.now();
this.isOpened = false;
this.isAnimating = false;
}
toggle() {
this.isAnimating = true;
const range = 4 * Math.random() + 6;
for (var i = 0; i < this.numPoints; i++) {
const radian = i / (this.numPoints - 1) * Math.PI;
this.delayPointsArray[i] = (Math.sin(-radian) + Math.sin(-radian * range) + 2) / 4 * this.delayPointsMax;
}
if (this.isOpened === false) {
this.open();
} else {
this.close();
}
}
open() {
this.isOpened = true;
this.elm.classList.add('is-opened');
this.timeStart = Date.now();
this.renderLoop();
}
close() {
this.isOpened = false;
this.elm.classList.remove('is-opened');
this.timeStart = Date.now();
this.renderLoop();
}
updatePath(time) {
const points = [];
for (var i = 0; i < this.numPoints + 1; i++) {
points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100
}
let str = '';
str += (this.isOpened) ? `M 0 0 V ${points[0]} ` : `M 0 ${points[0]} `;
for (var i = 0; i < this.numPoints - 1; i++) {
const p = (i + 1) / (this.numPoints - 1) * 100;
const cp = p - (1 / (this.numPoints - 1) * 100) / 2;
str += `C ${cp} ${points[i]} ${cp} ${points[i + 1]} ${p} ${points[i + 1]} `;
}
str += (this.isOpened) ? `V 0 H 0` : `V 100 H 0`;
return str;
}
render() {
if (this.isOpened) {
for (var i = 0; i < this.path.length; i++) {
this.path[i].setAttribute('d', this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * i)));
}
} else {
for (var i = 0; i < this.path.length; i++) {
this.path[i].setAttribute('d', this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * (this.path.length - i - 1))));
}
}
}
renderLoop() {
this.render();
if (Date.now() - this.timeStart < this.duration + this.delayPerPath * (this.path.length - 1) + this.delayPointsMax) {
requestAnimationFrame(() => {
this.renderLoop();
});
}
else {
this.isAnimating = false;
}
}
}
(function() {
const elmHamburger = document.querySelector('.hamburger');
const gNavItems = document.querySelectorAll('.global-menu__item');
const elmOverlay = document.querySelector('.shape-overlays');
const overlay = new ShapeOverlays(elmOverlay);
elmHamburger.addEventListener('click', () => {
if (overlay.isAnimating) {
return false;
}
overlay.toggle();
if (overlay.isOpened === true) {
elmHamburger.classList.add('is-opened-navi');
for (var i = 0; i < gNavItems.length; i++) {
gNavItems[i].classList.add('is-opened');
}
} else {
elmHamburger.classList.remove('is-opened-navi');
for (var i = 0; i < gNavItems.length; i++) {
gNavItems[i].classList.remove('is-opened');
}
}
});
}());
Can some one please explain this code? I don't really get how the paths are created using time,how the points are placed and how could I modify it.What is range used for? Why are trigonometral functions used for the delayPointsArray?
Basically it's this part that I don't get:
updatePath(time) {
const points = [];
for (var i = 0; i < this.numPoints + 1; i++) {
points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100
}
let str = '';
str += (this.isOpened) ? `M 0 0 V ${points[0]} ` : `M 0 ${points[0]} `;
for (var i = 0; i < this.numPoints - 1; i++) {
const p = (i + 1) / (this.numPoints - 1) * 100;
const cp = p - (1 / (this.numPoints - 1) * 100) / 2;
str += `C ${cp} ${points[i]} ${cp} ${points[i + 1]} ${p} ${points[i + 1]} `;
}
str += (this.isOpened) ? `V 0 H 0` : `V 100 H 0`;
return str;
}
render() {
if (this.isOpened) {
for (var i = 0; i < this.path.length; i++) {
this.path[i].setAttribute('d', this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * i)));
}
} else {
for (var i = 0; i < this.path.length; i++) {
this.path[i].setAttribute('d', this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * (this.path.length - i - 1))));
}
}
}
Why is time being used? What is the purpose of this:
points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100
If you look at how updatePath() is being called, it's like this:
this.updatePath(Date.now() - (this.timeStart + this.delayPerPath * i))
So the time value passed in is the difference between the current time, and the start time of the path we are working with.
So what then is the line of code you are interested in, doing?
points[i] = ease.cubicInOut(Math.min(Math.max(time - this.delayPointsArray[i], 0) / this.duration, 1)) * 100
I'm going to ignore delayPointsArray. It is modifying the start time slightly based on angle. Without seeing the full demo, I'm not sure of the reason for that.
The purpose of this line of code is to calculate how far through the current path's animation we are. The result is in the form of a coordinate value from 0 to 100.
It's doing a lot in that one line of code. So let's break down the individual steps.
Firstly, we are clamping the elapsed time to minimum of 0.
Math.max(time, 0)
In other words, anything before the animation start time becomes zero.
Then we divide by the animation's duration.
Math.max(time, 0) / duration
This will result in a value from 0, representing the start of the animation, to 1, representing the end of the animation. However, the value might also be greater than 1 if the elapsed time is after the end of the animation. Hence the next step.
Now clamp this value to a maximum of 1.
Math.min( Math.max(time, 0) / duration, 1)
We now have a value >= 0 and <= 1 whichdescribes where in the course of the animation, the path is supposed to be. 0 if we should be at the animations start position. 1 if we should be at the animations end position. And somewhere in between if the animation is in progress.
However this value is strictly linear, corresponding with the progression of time. And usually linear movement is not what you want. It is unnatural. Objects accelarate when the start moving and decelerate when the come to a stop. That will be what the easeInOut() function will be doing. If you are not familiar with easing curves, take a look at the diagram below.
Source: Google: The Basics of Easing
So we pass in a linear time value from 0..1 (horizontal axis). It will return a modified value that takes into account acceleration and deceleration.
The final step is to multiply by 100, to convert to a final coordinate value (0..100).
Hope this helps.
This function can paginate an array of items:
function paginate(res,page,page_size,max_page_size) {
page=page||1;
page_size=page_size||10;
max_page_size=max_page_size||100;
page_size=page_size>max_page_size?max_page_size:page_size;
var pages=Math.round( res.length / page_size),
items=[];
for( var p=1; p <= pages; p++) {
var start= Math.round( page_size * (p-1) );
items.push( res.slice(start,start+page_size) );
}
return page < items.length?items[page-1]:items[ items.length-1 ];
}
having
res = array
page = current page
page_size = number of items to be returned in a page
max_page_size = max number of items for page
function paginate(res, page, page_size, max_page_size) {
page = page || 1;
page_size = page_size || 10;
max_page_size = max_page_size || 100;
page_size = page_size > max_page_size ? max_page_size : page_size;
var pages = Math.round(res.length / page_size),
items = [];
for (var p = 1; p <= pages; p++) {
var start = Math.round(page_size * (p - 1));
items.push(res.slice(start, start + page_size));
}
return page < items.length ? items[page - 1] : items[items.length - 1];
}
var list = Array.apply(null, Array(10)).map(function() {
return Array.apply(null, Array(Math.floor(Math.random() * 10 + 3))).map(function() {
return String.fromCharCode(Math.floor(Math.random() * (123 - 97) + 97));
}).join('')
});
console.log(list)
for (var i = 1; i < 8; i++) {
console.log("page", i, paginate(list, i, 2, 2))
}
This function has a bug, since as soon as it reaches the last paginated result, it returns always the same page. Why? Is there an alternative solution?
[UPDATE]
The issue was in the indexes calculation, this is the right version:
function paginate(res,page,page_size,max_page_size) {
page=page||1;
page_size=page_size||10;
max_page_size=max_page_size||100;
page_size=page_size>max_page_size?max_page_size:page_size;
var pages=Math.ceil( res.length / page_size),
items=[];
for( var p=1; p <= pages; p++) {
var start= page_size * (p-1)
items.push( res.slice(start,start+page_size) );
}
return page <= items.length?items[page-1]:[];
}
There were 2 errors: Math.round instead of Math.ceil and the last ternary operator page <= items.length?items[page-1]:[];
Haven't identified the exact issue with your code yet, but I remember struggling alot with keeping state and resetting the index when I reached the end of the array while using rounding functions.
In the end I just made a 'chunker' to divide an array into chunks and hooked that up to something that rendered the array contents using the previous() and next() methods to grab the correct art of the array. Which page it actualy represents in the view, is up to the view to cache.
var Chunker = function Chunker( config ) {
if ( !Array.isArray( config.data ) ) {
throw new Error('chunker source is not an array');
return false;
}
this.data = config.data;
this.index = 0;
this.length = config.len || config.data.length; // Amount of items per chunk. When not defined, will return the entire array.
};
Chunker.prototype = {
'constructor' : Chunker,
'index' : function index( number ) { // => current index (getter) || => this (setter)
if ( number !== undefined ) {
this.index = index;
return this;
}
else return this.index;
},
'current' : function current() { // => { "index", "data" }
return {
"index" : this.index,
"data" : this.data.slice( this.index, this.index + this.length )
};
},
'next' : function next() { // => next chunk
this.index += this.length;
if ( this.index > this.data.length ) this.index = this.data.length - (this.data.length % this.length);
return this.current();
},
'previous' : function previous() { // => previous chunk
this.index -= this.length;
if (this.index < 0 ) this.index = 0;
return this.current();
},
'reset' : function reset() { // => this
this.index(0);
return this;
}
};
I am having an issue with the following code that simulates a card deck.
The deck is created properly (1 array containing 4 arrays (suits) containing 13 elements each (face values)) and when I use the G.test(); function it is correctly pulling 13 random cards but then returns 39x "Empty" (A total of 52).
I hate to ask for help, but I have left the problem overnight and then some and I still cannot find the reason that this is happening. I appreciate any and all insight that can be offered.
var G = {};
G.cards = [[], [], [], []];
G.newCard = function(v) { //currently a useless function, tried a few things
return v;
};
G.deck = {
n: function() { //new deck
var x; var list = [];
list.push(G.newCard("A"));
for (x = 2; x <= 10; x += 1) {
list.push(G.newCard(x.toString()));
}
list.push(G.newCard("J"), G.newCard("Q"), G.newCard("K"));
for (x = 0; x < G.cards.length; x += 1) {
G.cards[x] = list;
}
},
d: function() { //random card - returns suit & value
var s; var c; var v; var drawn = false; var n;
s = random(0, G.cards.length);
c = random(0, G.cards[s].length);
n = 0;
while (!drawn) {
if (G.cards[s].length > 0) {
if (G.cards[s][c]) {
v = G.cards[s].splice(c, 1);
drawn = true;
} else {
c = random(0, G.cards[s].length);
}
} else {
s = (s + 1 >= G.cards.length) ? 0 : s + 1;
n += 1;
console.log(s);
if (n >= G.cards.length) {
console.log(n);
return "Empty";
}
}
}
return {s: s, v: v[0]};
},
}; //G.deck
G.test = function() {
var x; var v;
G.deck.n();
for (x = 0; x < 52; x += 1) {
v = G.deck.d();
console.log(v);
}
};
Replace
for (x = 0; x < G.cards.length; x += 1) {
G.cards[x] = list;
}
with
for (x = 0; x < G.cards.length; x += 1) {
G.cards[x] = list.slice();
}
as this prevents all elements of G.cards[x] binding to the same (single) array instance.
If all elements bind to the same instance, mutating one element equals mutating all elements. list.slice() creates a new copy of list and thus a new array instance to prevent the aforementioned issue.
I won't go through your code, but I built a code that will do what you wanted. I only built this for one deck and not multiple deck play. There are two functions, one will generate the deck, and the other will drawn cards from the deck, bases on how many hands you need and how many cards you wanted for each hand. One a card is drawn, it will not be re-drawn. I might publish a short article for how a card dealing program work or similar in the short future at http://kevinhng86.iblog.website.
function random(min, max){
return Math.floor(Math.random() * (max - min)) + min;
}
function deckGenerate(){
var output = [];
var face = {1: "A", 11: "J", 12: "Q", 13: "K"};
// Heart Space Diamond & Club;
var suit = ["H", "S", "D", "C"];
// Delimiter between card identification and suit identification.
var d = "-";
for(i = 0; i < 4; i++){
output[i] = [];
for(ind = 0; ind < 13; ind++ ){
card = (ind + 1);
output[i][ind] = (card > 10) || (card === 1)? face[card] + d + suit[i] : card.toString() + d + suit[i];
}
}
return output;
}
function randomCard(deck, hand, card){
var output = [];
var randS = 0;
var randC = 0;
if( hand * card > 52 ) throw("Too many card, I built this for one deck only");
for(i = 0; i < hand; i++){
output[i] = [];
for(ind = 0; ind < card; ind++){
randS = random(0, deck.length);
randC = random(0, deck[randS].length);
output[i][ind] = deck[randS][randC];
deck[randS].splice(randC,1);
if(deck[randS].length === 0) deck.splice(randS,1);
}
}
document.write( JSON.stringify(deck, null, 2) );
return output;
}
var deck = deckGenerate()
document.write( JSON.stringify(deck, null, 2) );
document.write("<br><br>");
var randomhands = randomCard(deck, 5, 8);
document.write("<br><br>");
document.write("<br><br>");
document.write( JSON.stringify(randomhands, null, 2) );
I'm currently working on a d3 project and I'm trying to display bar charts with a huge range of values, both positive and negative.
I saw online a walkaround using d3.scale.sqrt() or displaying two log scale but I was wondering if I could create my own scale.
What I have in mind is a mix between a log scale for negative values, a linear scale for values between [-e, e] and a regular log scale for positive values.
Something that might look like that: http://img15.hostingpics.net/pics/12746197ln.png
y = -log(-x) if x < -e
y = x/e if -e <= x <= e
y = log(x) if x > e
Do you think that it might be possible ?
I also created a JSFiddle to sum it up.
Thanks,
Here is one solution I found: JSFiddle
It might be a weird way, but it's working I think, if you have any pieces of advice on improvement, don't hesitate. I think I made mistakes, especially on log base and ticks.
Here is the function I created, based on d3.js itself.
(function() {
scalepnlog = {
init: function(){
return d3_scale_pnlog(d3.scale.linear().domain([ 0, 1 ]), [ 1, 10 ]);
}
}
function d3_scaleExtent(domain) {
var start = domain[0], stop = domain[domain.length - 1];
return start < stop ? [ start, stop ] : [ stop, start ];
}
var d3_scale_logFormat = d3.format(".0e"), d3_scale_logNiceNegative = {
floor: function(x) {
return -Math.ceil(-x);
},
ceil: function(x) {
return -Math.floor(-x);
}
};
function sign(x){
return x >= 0 ? 1:-1;
}
function d3_scale_pnlog(linear, domain) {
function pnlog(x) {
return (x >= Math.E || x <= -Math.E) ? sign(x)*Math.log(Math.abs(x)) : x/Math.E;
}
function pnpow(x) {
return (x >= 1 || x <= -1 )? sign(x)*Math.pow(Math.E,Math.abs(x)) : Math.E*x;
}
function scale(x) {
return linear(pnlog(x));
}
scale.invert = function(x) {
return pnpow(linear.invert(x));
};
scale.domain = function(x) {
if (!arguments.length) return domain;
linear.domain((domain = x.map(Number)).map(pnlog));
return scale;
};
scale.nice = function() {
var niced = d3_scale_nice(domain.map(pnlog), positive ? Math : d3_scale_logNiceNegative);
linear.domain(niced);
domain = niced.map(pow);
return scale;
};
scale.ticks = function() {
var extent = d3_scaleExtent(domain), ticks = [], u = extent[0], v = extent[1], i = Math.floor(pnlog(u)), j = Math.ceil(pnlog(v)), n = 10 % 1 ? 2 : 10;
if (isFinite(j - i)) {
for (;i < j; i++) for (var k = 1; k < n; k++) ticks.push(pnpow(i) * k);
ticks.push(pnpow(i));
for (i = 0; ticks[i] < u; i++) {}
for (j = ticks.length; ticks[j - 1] > v; j--) {}
ticks = ticks.slice(i, j);
}
return ticks;
};
scale.tickFormat = function(n, format) {
if (!arguments.length) return d3_scale_logFormat;
if (arguments.length < 2) format = d3_scale_logFormat; else if (typeof format !== "function") format = d3.format(format);
var k = Math.max(1, 10 * n / scale.ticks().length);
return function(d) {
var i = d / pnpow(Math.round(pnlog(d)));
if (i * 10 < 10 - .5) i *= 10;
return i <= k ? format(d) : "";
};
};
scale.copy = function() {
return d3_scale_pnlog(linear.copy(), domain);
};
return d3.rebind(scale, linear, "range", "rangeRound", "interpolate", "clamp");
}
})();
I don't really know what I'm doing but basically, I created pnlog and pnpow, its reciprocal, and added the different d3 functions needed until it worked.
Here they are :
function pnlog(x) {
return (x >= Math.E || x <= -Math.E) ? sign(x)*Math.log(Math.abs(x)) : x/Math.E;
}
and
function pnpow(x) {
return (x >= 1 || x <= -1 )? sign(x)*Math.pow(Math.E,Math.abs(x)) : Math.E*x;
}
So I have a random javascript array of names...
[#larry,#nicholas,#notch] etc.
They all start with the # symbol. I'd like to sort them by the Levenshtein Distance so that the the ones at the top of the list are closest to the search term. At the moment, I have some javascript that uses jQuery's .grep() on it using javascript .match() method around the entered search term on key press:
(code edited since first publish)
limitArr = $.grep(imTheCallback, function(n){
return n.match(searchy.toLowerCase())
});
modArr = limitArr.sort(levenshtein(searchy.toLowerCase(), 50))
if (modArr[0].substr(0, 1) == '#') {
if (atRes.childred('div').length < 6) {
modArr.forEach(function(i){
atRes.append('<div class="oneResult">' + i + '</div>');
});
}
} else if (modArr[0].substr(0, 1) == '#') {
if (tagRes.children('div').length < 6) {
modArr.forEach(function(i){
tagRes.append('<div class="oneResult">' + i + '</div>');
});
}
}
$('.oneResult:first-child').addClass('active');
$('.oneResult').click(function(){
window.location.href = 'http://hashtag.ly/' + $(this).html();
});
It also has some if statements detecting if the array contains hashtags (#) or mentions (#). Ignore that. The imTheCallback is the array of names, either hashtags or mentions, then modArr is the array sorted. Then the .atResults and .tagResults elements are the elements that it appends each time in the array to, this forms a list of names based on the entered search terms.
I also have the Levenshtein Distance algorithm:
var levenshtein = function(min, split) {
// Levenshtein Algorithm Revisited - WebReflection
try {
split = !("0")[0]
} catch(i) {
split = true
};
return function(a, b) {
if (a == b)
return 0;
if (!a.length || !b.length)
return b.length || a.length;
if (split) {
a = a.split("");
b = b.split("")
};
var len1 = a.length + 1,
len2 = b.length + 1,
I = 0,
i = 0,
d = [[0]],
c, j, J;
while (++i < len2)
d[0][i] = i;
i = 0;
while (++i < len1) {
J = j = 0;
c = a[I];
d[i] = [i];
while(++j < len2) {
d[i][j] = min(d[I][j] + 1, d[i][J] + 1, d[I][J] + (c != b[J]));
++J;
};
++I;
};
return d[len1 - 1][len2 - 1];
}
}(Math.min, false);
How can I work with algorithm (or a similar one) into my current code to sort it without bad performance?
UPDATE:
So I'm now using James Westgate's Lev Dist function. Works WAYYYY fast. So performance is solved, the issue now is using it with source...
modArr = limitArr.sort(function(a, b){
levDist(a, searchy)
levDist(b, searchy)
});
My problem now is general understanding on using the .sort() method. Help is appreciated, thanks.
Thanks!
I wrote an inline spell checker a few years ago and implemented a Levenshtein algorithm - since it was inline and for IE8 I did quite a lot of performance optimisation.
var levDist = function(s, t) {
var d = []; //2d matrix
// Step 1
var n = s.length;
var m = t.length;
if (n == 0) return m;
if (m == 0) return n;
//Create an array of arrays in javascript (a descending loop is quicker)
for (var i = n; i >= 0; i--) d[i] = [];
// Step 2
for (var i = n; i >= 0; i--) d[i][0] = i;
for (var j = m; j >= 0; j--) d[0][j] = j;
// Step 3
for (var i = 1; i <= n; i++) {
var s_i = s.charAt(i - 1);
// Step 4
for (var j = 1; j <= m; j++) {
//Check the jagged ld total so far
if (i == j && d[i][j] > 4) return n;
var t_j = t.charAt(j - 1);
var cost = (s_i == t_j) ? 0 : 1; // Step 5
//Calculate the minimum
var mi = d[i - 1][j] + 1;
var b = d[i][j - 1] + 1;
var c = d[i - 1][j - 1] + cost;
if (b < mi) mi = b;
if (c < mi) mi = c;
d[i][j] = mi; // Step 6
//Damerau transposition
if (i > 1 && j > 1 && s_i == t.charAt(j - 2) && s.charAt(i - 2) == t_j) {
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost);
}
}
}
// Step 7
return d[n][m];
}
I came to this solution:
var levenshtein = (function() {
var row2 = [];
return function(s1, s2) {
if (s1 === s2) {
return 0;
} else {
var s1_len = s1.length, s2_len = s2.length;
if (s1_len && s2_len) {
var i1 = 0, i2 = 0, a, b, c, c2, row = row2;
while (i1 < s1_len)
row[i1] = ++i1;
while (i2 < s2_len) {
c2 = s2.charCodeAt(i2);
a = i2;
++i2;
b = i2;
for (i1 = 0; i1 < s1_len; ++i1) {
c = a + (s1.charCodeAt(i1) === c2 ? 0 : 1);
a = row[i1];
b = b < a ? (b < c ? b + 1 : c) : (a < c ? a + 1 : c);
row[i1] = b;
}
}
return b;
} else {
return s1_len + s2_len;
}
}
};
})();
See also http://jsperf.com/levenshtein-distance/12
Most speed was gained by eliminating some array usages.
Updated: http://jsperf.com/levenshtein-distance/5
The new Revision annihilates all other benchmarks. I was specifically chasing Chromium/Firefox performance as I don't have an IE8/9/10 test environment, but the optimisations made should apply in general to most browsers.
Levenshtein Distance
The matrix to perform Levenshtein Distance can be reused again and again. This was an obvious target for optimisation (but be careful, this now imposes a limit on string length (unless you were to resize the matrix dynamically)).
The only option for optimisation not pursued in jsPerf Revision 5 is memoisation. Depending on your use of Levenshtein Distance, this could help drastically but was omitted due to its implementation specific nature.
// Cache the matrix. Note this implementation is limited to
// strings of 64 char or less. This could be altered to update
// dynamically, or a larger value could be used.
var matrix = [];
for (var i = 0; i < 64; i++) {
matrix[i] = [i];
matrix[i].length = 64;
}
for (var i = 0; i < 64; i++) {
matrix[0][i] = i;
}
// Functional implementation of Levenshtein Distance.
String.levenshteinDistance = function(__this, that, limit) {
var thisLength = __this.length, thatLength = that.length;
if (Math.abs(thisLength - thatLength) > (limit || 32)) return limit || 32;
if (thisLength === 0) return thatLength;
if (thatLength === 0) return thisLength;
// Calculate matrix.
var this_i, that_j, cost, min, t;
for (i = 1; i <= thisLength; ++i) {
this_i = __this[i-1];
for (j = 1; j <= thatLength; ++j) {
// Check the jagged ld total so far
if (i === j && matrix[i][j] > 4) return thisLength;
that_j = that[j-1];
cost = (this_i === that_j) ? 0 : 1; // Chars already match, no ++op to count.
// Calculate the minimum (much faster than Math.min(...)).
min = matrix[i - 1][j ] + 1; // Deletion.
if ((t = matrix[i ][j - 1] + 1 ) < min) min = t; // Insertion.
if ((t = matrix[i - 1][j - 1] + cost) < min) min = t; // Substitution.
matrix[i][j] = min; // Update matrix.
}
}
return matrix[thisLength][thatLength];
};
Damerau-Levenshtein Distance
jsperf.com/damerau-levenshtein-distance
Damerau-Levenshtein Distance is a small modification to Levenshtein Distance to include transpositions. There is very little to optimise.
// Damerau transposition.
if (i > 1 && j > 1 && this_i === that[j-2] && this[i-2] === that_j
&& (t = matrix[i-2][j-2]+cost) < matrix[i][j]) matrix[i][j] = t;
Sorting Algorithm
The second part of this answer is to choose an appropriate sort function. I will upload optimised sort functions to http://jsperf.com/sort soon.
I implemented a very performant implementation of levenshtein distance calculation if you still need this.
function levenshtein(s, t) {
if (s === t) {
return 0;
}
var n = s.length, m = t.length;
if (n === 0 || m === 0) {
return n + m;
}
var x = 0, y, a, b, c, d, g, h, k;
var p = new Array(n);
for (y = 0; y < n;) {
p[y] = ++y;
}
for (; (x + 3) < m; x += 4) {
var e1 = t.charCodeAt(x);
var e2 = t.charCodeAt(x + 1);
var e3 = t.charCodeAt(x + 2);
var e4 = t.charCodeAt(x + 3);
c = x;
b = x + 1;
d = x + 2;
g = x + 3;
h = x + 4;
for (y = 0; y < n; y++) {
k = s.charCodeAt(y);
a = p[y];
if (a < c || b < c) {
c = (a > b ? b + 1 : a + 1);
}
else {
if (e1 !== k) {
c++;
}
}
if (c < b || d < b) {
b = (c > d ? d + 1 : c + 1);
}
else {
if (e2 !== k) {
b++;
}
}
if (b < d || g < d) {
d = (b > g ? g + 1 : b + 1);
}
else {
if (e3 !== k) {
d++;
}
}
if (d < g || h < g) {
g = (d > h ? h + 1 : d + 1);
}
else {
if (e4 !== k) {
g++;
}
}
p[y] = h = g;
g = d;
d = b;
b = c;
c = a;
}
}
for (; x < m;) {
var e = t.charCodeAt(x);
c = x;
d = ++x;
for (y = 0; y < n; y++) {
a = p[y];
if (a < c || d < c) {
d = (a > d ? d + 1 : a + 1);
}
else {
if (e !== s.charCodeAt(y)) {
d = c + 1;
}
else {
d = c;
}
}
p[y] = d;
c = a;
}
h = d;
}
return h;
}
It was my answer to a similar SO question
Fastest general purpose Levenshtein Javascript implementation
Update
A improved version of the above is now on github/npm see
https://github.com/gustf/js-levenshtein
The obvious way of doing this is to map each string to a (distance, string) pair, then sort this list, then drop the distances again. This way you ensure the levenstein distance only has to be computed once. Maybe merge duplicates first, too.
I would definitely suggest using a better Levenshtein method like the one in #James Westgate's answer.
That said, DOM manipulations are often a great expense. You can certainly improve your jQuery usage.
Your loops are rather small in the example above, but concatenating the generated html for each oneResult into a single string and doing one append at the end of the loop will be much more efficient.
Your selectors are slow. $('.oneResult') will search all elements in the DOM and test their className in older IE browsers. You may want to consider something like atRes.find('.oneResult') to scope the search.
In the case of adding the click handlers, we may want to do one better avoid setting handlers on every keyup. You could leverage event delegation by setting a single handler on atRest for all results in the same block you are setting the keyup handler:
atRest.on('click', '.oneResult', function(){
window.location.href = 'http://hashtag.ly/' + $(this).html();
});
See http://api.jquery.com/on/ for more info.
I just wrote an new revision: http://jsperf.com/levenshtein-algorithms/16
function levenshtein(a, b) {
if (a === b) return 0;
var aLen = a.length;
var bLen = b.length;
if (0 === aLen) return bLen;
if (0 === bLen) return aLen;
var len = aLen + 1;
var v0 = new Array(len);
var v1 = new Array(len);
var i = 0;
var j = 0;
var c2, min, tmp;
while (i < len) v0[i] = i++;
while (j < bLen) {
c2 = b.charAt(j++);
v1[0] = j;
i = 0;
while (i < aLen) {
min = v0[i] - (a.charAt(i) === c2 ? 1 : 0);
if (v1[i] < min) min = v1[i];
if (v0[++i] < min) min = v0[i];
v1[i] = min + 1;
}
tmp = v0;
v0 = v1;
v1 = tmp;
}
return v0[aLen];
}
This revision is faster than the other ones. Works even on IE =)